PreToolUse hook with permissionDecision: "deny" emits redundant <system-reminder>
Summary
A PreToolUse hook returning permissionDecision: "deny" via the documented hookSpecificOutput format causes the runtime to send two distinct messages to the model in the same turn:
- The expected denial (rendered as the tool's
<error>block carryingpermissionDecisionReason). - Separately, a
<system-reminder>titled:
> PreToolUse:Bash hook blocking error from command: "/path/to/hook"
…repeating the full deny reason text.
Only (1) is documented. (2) is redundant and creates the same "did the tool run anyway?" perception that motivated moving away from the deprecated continue: false shape — except this time the dual-message comes from a different runtime emit path that the deprecation cleanup didn't cover.
Hook output (correct shape)
The hook prints valid JSON on stdout and exits 0, no stderr:
jq -n \
--arg cmd "$COMMAND" \
'{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: ("denied: " + $cmd)
}
}'
This matches the hook contract docs — permissionDecision: "deny" with a permissionDecisionReason is the canonical refusal channel.
Reproduction
Configure a PreToolUse:Bash matcher pointing at a script that emits the JSON above (any deny condition will do — e.g., reject commands containing ;). Run a Bash tool call that triggers the deny. Observe in the model's view:
<error>permissionDecisionReason text here</error>
<system-reminder>
PreToolUse:Bash hook blocking error from command: "/path/to/hook": permissionDecisionReason text here
</system-reminder>
Both blocks are present. The <system-reminder> is added by the runtime independent of the hook's own output.
Expected
When a PreToolUse hook returns permissionDecision: "deny", the tool's <error> block is the canonical channel. No follow-up <system-reminder> should be emitted, mirroring how permissionDecision: "allow" doesn't produce one.
Suggested investigation
The literal PreToolUse:Bash hook blocking error from command: likely lives in the runtime as a separate emit path from the one the deprecated continue: false block field used. A previous fix to the deprecated path (silenced its "PreToolUse:Bash hook stopped continuation" reminder) did not reach this one.
May also affect other PreToolUse matchers (Write/Edit guards) using the same return shape — untested but logically symmetric.
Environment
- Claude Code 2.1.126
- macOS 26.4.1, Darwin 25.4.0 arm64
Workaround
None at the agent or hook author level. The hook output is already in the documented shape; switching back to continue: false would re-trigger the other reminder this format was designed to silence. The fix must be in the runtime.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗