When multiple PreToolUse hooks deny the same tool call, only one denial reason is surfaced to the model
Summary
When two (or more) PreToolUse hooks each return permissionDecision: "deny" for the same tool call, Claude Code correctly blocks the call, but only one hook's permissionDecisionReason is surfaced to the model (and recorded in the session transcript). The other denial reasons are silently dropped, even though all hooks demonstrably execute.
Environment
- Claude Code 2.1.218 (CLI, headless
-pmode; also reproduced interactively) - macOS 26.5.2 (arm64)
Repro
Project .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "/tmp/hooks/deny-a.sh" },
{ "type": "command", "command": "/tmp/hooks/deny-b.sh" }
]
}
]
}
}
/tmp/hooks/deny-a.sh (deny-b.sh identical except the reason string and log line):
#!/bin/sh
payload=$(cat)
echo "invoked" >> /tmp/hooks/a.log
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"BLOCKED by hook A"}}\n'
exit 0
Then:
claude -p 'Run exactly this bash command once: echo hello. If blocked, report verbatim every block reason you received.' --allowedTools Bash
Observed
- The tool call is blocked (correct, deny wins).
- Both
a.logandb.loggain entries — both hooks ran. - The model receives only ONE of the two reasons; the transcript likewise contains only that one. Which hook's reason wins did not follow registration order in our tests (the same hook's reason was surfaced regardless of its position in the array), so there appears to be no way to control or predict the surfaced reason.
Why it matters
The model acts on the reason it sees. With layered guards (e.g. a generic destructive-command hook plus a project-specific policy hook that points at the sanctioned alternative workflow), the dropped reason is often the one carrying the actionable remediation. The model then "fixes" the surfaced objection, retries, and gets denied again by the hook whose guidance it never saw — a wasted denial loop that surfacing both reasons up front would avoid. It also under-reports policy for auditing: the transcript suggests one guard fired when several did.
Expected / requested
When multiple matching PreToolUse hooks deny, surface all distinct denial reasons (deduplicated, optionally length-capped) to the model — e.g. concatenated or as a list — rather than exactly one. Enforcement behavior (deny wins) is already correct and should not change.