wrapper-guard.sh: BLOCK verdict text goes only to stdout - Claude Code PreToolUse blocks are silent #1493

Open
opened 2026-09-04 17:36:00 +00:00 by orch-01 · 0 comments
Member

Defect

wrapper-guard.sh prints every BLOCK verdict to stdout (all cat <<EOF block-message sites) and exits 2. Harness PreToolUse contracts differ in which channel they surface on exit 2:

  • Claude Code feeds only stderr back to the model on exit 2. The agent sees hook error: ... No stderr output and the entire block rationale is lost: the block is effectively silent.
  • The pi wrapper-guard listener reads res.stdout || res.stderr, so pi users see the message.

The guard's own contract states every block emits a message. On Claude Code, none is delivered.

Repro (direct, no harness)

Input {"tool_name":"Bash","tool_input":{"command":"curl -s \"$U/api/v1/x/$V\" -d x"}} piped to the guard:

  • rc=2
  • stdout: 896 bytes (full BLOCK verdict)
  • stderr: 0 bytes

The hermetic suite captures 2>&1 at every assertion site, so it cannot detect the channel and passes 310/310 while the Claude path is silent.

Related observation (by design, documented here because it compounds this defect): the guard judges the whole submitted command text, so a command whose literal text embeds a guarded payload shape (e.g. a test fixture string) is itself blocked. That is correct per the header, but combined with this defect the operator receives an unexplained block.

Fix shape

  1. Emit the block message on stderr (or emit the Claude verdict JSON {"decision":"block","reason":...} on stdout with human-readable text on stderr), keeping pi compatibility (read stderr first, or dual-emit).
  2. Add suite arms that assert the verdict text lands on the channel each harness reads: a stderr assertion on the exit-2 path and a stdout assertion for pi consumers. No 2>&1 captures.

Environment: bash 5.x; packages/mosaic/framework/tools/git/wrapper-guard.sh (current next) as deployed to framework tool trees; Claude Code and pi harnesses.

## Defect `wrapper-guard.sh` prints every BLOCK verdict to **stdout** (all `cat <<EOF` block-message sites) and exits 2. Harness PreToolUse contracts differ in which channel they surface on exit 2: - Claude Code feeds only **stderr** back to the model on exit 2. The agent sees `hook error: ... No stderr output` and the entire block rationale is lost: the block is effectively silent. - The pi wrapper-guard listener reads `res.stdout || res.stderr`, so pi users see the message. The guard's own contract states every block emits a message. On Claude Code, none is delivered. ## Repro (direct, no harness) Input `{"tool_name":"Bash","tool_input":{"command":"curl -s \"$U/api/v1/x/$V\" -d x"}}` piped to the guard: - rc=2 - stdout: 896 bytes (full BLOCK verdict) - stderr: 0 bytes The hermetic suite captures `2>&1` at every assertion site, so it cannot detect the channel and passes 310/310 while the Claude path is silent. Related observation (by design, documented here because it compounds this defect): the guard judges the whole submitted command text, so a command whose literal text embeds a guarded payload shape (e.g. a test fixture string) is itself blocked. That is correct per the header, but combined with this defect the operator receives an unexplained block. ## Fix shape 1. Emit the block message on **stderr** (or emit the Claude verdict JSON `{"decision":"block","reason":...}` on stdout with human-readable text on stderr), keeping pi compatibility (read stderr first, or dual-emit). 2. Add suite arms that assert the verdict text lands on the channel each harness reads: a stderr assertion on the exit-2 path and a stdout assertion for pi consumers. No `2>&1` captures. Environment: bash 5.x; `packages/mosaic/framework/tools/git/wrapper-guard.sh` (current `next`) as deployed to framework tool trees; Claude Code and pi harnesses.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1493