PreToolUse hook with permissionDecision: "deny" emits redundant <system-reminder>

Resolved 💬 1 comment Opened May 4, 2026 by arechste Closed May 6, 2026

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:

  1. The expected denial (rendered as the tool's <error> block carrying permissionDecisionReason).
  2. 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 docspermissionDecision: "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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗