A PreToolUse policy *deny* is surfaced to the model as a "hook error"
Type: feature-request
Environment
- Claude Code version: 2.1.220
- OS: macOS 26.5.2 (Darwin 25.5.0)
- Platform: Claude subscription
What happened
When a PreToolUse hook deliberately denies a tool call (exit 2, with an explanation on stderr), the model receives it as:
PreToolUse:<ToolName> hook error: [<path to hook>]: <explanation>
The word error is doing the wrong job here. A deny is a policy decision with a stable outcome; an error implies something went wrong and might not go wrong next time. In our transcripts this framing appears in 263 of 5 096 transcript files, and it visibly nudges the model toward retrying — sometimes with a slightly reworded payload, which fails identically because the policy has not changed.
It also makes real hook failures indistinguishable from intentional denies: a hook that crashes on a syntax error and a hook that refuses a call both surface as "hook error", which makes debugging a hook setup harder than it needs to be.
Expected
Distinguish the two outcomes in the message the model sees, for example:
- deny →
Blocked by policy (PreToolUse:<ToolName>, <hook name>): <reason>— plus, ideally, a hint that retrying the same call will produce the same result. - hook crash / non-zero exit that is not a deny →
Hook failed (PreToolUse:<ToolName>, <hook name>): <stderr>.
A structured field in the hook's JSON output (decision: "deny" vs. an unexpected failure) would let the harness label these correctly without guessing.
Repro
- Register a
PreToolUsehook that exits 2 with an explanatory message on stderr for some tool. - Trigger that tool.
- Observe the model receives
PreToolUse:<Tool> hook error: ...and frequently retries the same call.
Impact
Wasted retries on decisions that are deterministic by construction, and a harder time telling a working policy hook from a broken one. Low implementation cost — this is a wording and classification change, not new machinery.