wrapper-guard blocks with no stderr: 'No stderr output' gives the agent nothing to comply with #1208

Open
opened 2026-08-13 15:57:59 +00:00 by Ghost · 0 comments

Summary

wrapper-guard.sh blocked a PreToolUse:Bash call and emitted nothing explaining why. The
entire feedback to the agent was:

PreToolUse:Bash hook error: [/src/jarvis-brain/scripts/hooks/wrapper-guard.sh]: No stderr output

No rule name, no matched pattern, no remediation. "No stderr output" is the harness reporting that
the guard said nothing — the guard's own message is absent, not merely terse.

How it was hit

While measuring #1197 I ran a loop whose loop variable contained checkout command strings. The
guard inspected the composed command, matched, and denied. The command was a measurement harness
that would never have performed a checkout; but that is not the problem — the problem is that when
the guard denied it, it gave nothing to act on.

The practical effect: the agent cannot tell a correct block from a misfire, cannot learn the rule,
and cannot rewrite the command into a compliant form. The only way forward is to guess. I worked
around it by moving the strings into a file so the command line no longer contained them — which is
the wrong lesson for a guard to teach, because evading detection is easier than complying with a
rule you cannot read.

Why it matters more than a usability nit

A guard that blocks silently is indistinguishable from a broken guard. Both produce "denied, no
reason." That ambiguity trains agents to route around the guard rather than satisfy it, and it
hides genuine misfires: a false positive and a true positive look identical from the outside.

This is the same family as the fail-open defects already filed today, inverted — there, success was
reported with nothing measured; here, failure is reported with nothing explained. Both break the
link between a gate's verdict and its evidence.

Proposed acceptance criteria

  • Every deny path writes to stderr: the rule that matched, the substring/pattern that triggered
    it, and the compliant alternative (the wrapper to use, or the correct target location).
  • No deny path can exit without a message — assert this in the test suite, not by inspection.
  • Distinguish "this command is forbidden" from "the guard itself failed"; today both surface as
    the same opaque hook error.
  • The guard should match on the command that will actually execute, so that a string appearing
    only as data (a loop variable, a heredoc, a quoted argument to echo) is not treated as an
    action. If that distinction is impractical, the deny message must at least name the matched
    text so the operator can see it matched data.
  • Regression test asserting non-empty stderr for each deny rule.
## Summary `wrapper-guard.sh` blocked a `PreToolUse:Bash` call and emitted **nothing** explaining why. The entire feedback to the agent was: ``` PreToolUse:Bash hook error: [/src/jarvis-brain/scripts/hooks/wrapper-guard.sh]: No stderr output ``` No rule name, no matched pattern, no remediation. "No stderr output" is the harness reporting that the guard said nothing — the guard's own message is absent, not merely terse. ## How it was hit While measuring #1197 I ran a loop whose **loop variable** contained checkout command strings. The guard inspected the composed command, matched, and denied. The command was a measurement harness that would never have performed a checkout; but that is not the problem — the problem is that when the guard denied it, it gave nothing to act on. The practical effect: the agent cannot tell a correct block from a misfire, cannot learn the rule, and cannot rewrite the command into a compliant form. The only way forward is to guess. I worked around it by moving the strings into a file so the command line no longer contained them — which is the *wrong* lesson for a guard to teach, because evading detection is easier than complying with a rule you cannot read. ## Why it matters more than a usability nit A guard that blocks silently is indistinguishable from a broken guard. Both produce "denied, no reason." That ambiguity trains agents to route around the guard rather than satisfy it, and it hides genuine misfires: a false positive and a true positive look identical from the outside. This is the same family as the fail-open defects already filed today, inverted — there, success was reported with nothing measured; here, failure is reported with nothing explained. Both break the link between a gate's verdict and its evidence. ## Proposed acceptance criteria - [ ] Every deny path writes to stderr: the rule that matched, the substring/pattern that triggered it, and the compliant alternative (the wrapper to use, or the correct target location). - [ ] No deny path can exit without a message — assert this in the test suite, not by inspection. - [ ] Distinguish "this command is forbidden" from "the guard itself failed"; today both surface as the same opaque hook error. - [ ] The guard should match on the command that will actually execute, so that a string appearing only as data (a loop variable, a heredoc, a quoted argument to `echo`) is not treated as an action. If that distinction is impractical, the deny message must at least name the matched text so the operator can see it matched data. - [ ] Regression test asserting non-empty stderr for each deny rule.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1208