Summary
Related issues: #20157, #12667
When a PreToolUse hook blocks a tool and provides complete disclosure to both user (systemMessage) and agent (permissionDecisionReason), Claude Code still displays "PreToolUse:Bash hook returned blocking error" for each blocking hook. This label is redundant -- the hook's own messages already communicate what happened and why.
With multiple blocking hooks, the repeated label creates significant visual noise:
Bash(python3 script.py)
⎿ PreToolUse:Bash hook returned blocking error
⎿ PreToolUse:Bash says: 🔒 SKILL ASSESSMENT REQUIRED: ...
⎿ PreToolUse:Bash hook returned blocking error
⎿ PreToolUse:Bash says: CHECKPOINT TRIGGER: ...
⎿ PreToolUse:Bash hook returned blocking error
⎿ PreToolUse:Bash says: 🔒 TOOL ACCESS GATED: ...
⎿ Error: Hook PreToolUse:Bash denied this tool
The three "hook returned blocking error" lines add no information the user doesn't already receive from the "says:" lines.
Expected Behavior
When a hook provides both systemMessage and permissionDecisionReason, the "hook returned blocking error" label should be suppressed, producing cleaner output:
Bash(python3 script.py)
⎿ PreToolUse:Bash says: 🔒 SKILL ASSESSMENT REQUIRED: ...
⎿ PreToolUse:Bash says: CHECKPOINT TRIGGER: ...
⎿ PreToolUse:Bash says: 🔒 TOOL ACCESS GATED: ...
⎿ Error: Hook PreToolUse:Bash denied this tool
The label remains valuable as a fallback when hooks provide incomplete or no output -- in that case it's the user's only signal that something happened.
If desired, blocking error could be incorporarted systemically, for example
Bash(python3 script.py)
⎿ PreToolUse:Bash (blocking error) says: 🔒 SKILL ASSESSMENT REQUIRED: ...
⎿ PreToolUse:Bash (blocking error) says: CHECKPOINT TRIGGER: ...
⎿ PreToolUse:Bash (blocking error) says: 🔒 TOOL ACCESS GATED: ...
⎿ Error: Hook PreToolUse:Bash denied this tool
Proposed Approaches
Option A (Preferred): Suppress the "hook returned blocking error" label by default when both systemMessage and permissionDecisionReason are populated. The hook's own disclosure replaces the need for the generic label.
Option B (Conservative): Add a new field (e.g., suppressBlockingLabel: true) to the modern hook JSON format. When specified, suppress the label -- but only if systemMessage and permissionDecisionReason are both populated. This makes suppression explicitly opt-in while guiding developers toward complete disclosure.
Impact
- 3 blocking hooks = 3 redundant "blocking error" lines the user must scan past
- Scales linearly with hook count -- more hooks, more noise
- Users may interpret repeated "error" labels as something going wrong, when the hooks are working as designed
Related Issues
#20157 proposes a UX framework for hook error disclosure with four principles (Agent Disclosure, User Disclosure, Consistency, Audience-Appropriate Formatting). The redundancy described here occurs specifically when hooks satisfy all four principles -- complete, correct disclosure to both audiences -- yet Claude Code still adds its own generic label on top.
#12667 describes a related problem for Stop hooks: the word "error" is used for intentional blocking behavior. This issue extends that observation to PreToolUse hooks: when a hook intentionally blocks and clearly communicates why, the "blocking error" label both mislabels the behavior (per #12667) and redundantly duplicates the hook's own messaging.
Environment
- Claude Code Version: current
- Platform: Linux (Ubuntu)
Summary
Related issues: #20157, #12667
When a PreToolUse hook blocks a tool and provides complete disclosure to both user (
systemMessage) and agent (permissionDecisionReason), Claude Code still displays "PreToolUse:Bash hook returned blocking error" for each blocking hook. This label is redundant -- the hook's own messages already communicate what happened and why.With multiple blocking hooks, the repeated label creates significant visual noise:
The three "hook returned blocking error" lines add no information the user doesn't already receive from the "says:" lines.
Expected Behavior
When a hook provides both
systemMessageandpermissionDecisionReason, the "hook returned blocking error" label should be suppressed, producing cleaner output:The label remains valuable as a fallback when hooks provide incomplete or no output -- in that case it's the user's only signal that something happened.
If desired, blocking error could be incorporarted systemically, for example
Proposed Approaches
Option A (Preferred): Suppress the "hook returned blocking error" label by default when both
systemMessageandpermissionDecisionReasonare populated. The hook's own disclosure replaces the need for the generic label.Option B (Conservative): Add a new field (e.g.,
suppressBlockingLabel: true) to the modern hook JSON format. When specified, suppress the label -- but only ifsystemMessageandpermissionDecisionReasonare both populated. This makes suppression explicitly opt-in while guiding developers toward complete disclosure.Impact
Related Issues
#20157 proposes a UX framework for hook error disclosure with four principles (Agent Disclosure, User Disclosure, Consistency, Audience-Appropriate Formatting). The redundancy described here occurs specifically when hooks satisfy all four principles -- complete, correct disclosure to both audiences -- yet Claude Code still adds its own generic label on top.
#12667 describes a related problem for Stop hooks: the word "error" is used for intentional blocking behavior. This issue extends that observation to PreToolUse hooks: when a hook intentionally blocks and clearly communicates why, the "blocking error" label both mislabels the behavior (per #12667) and redundantly duplicates the hook's own messaging.
Environment