Skip to content

[UX] "hook returned blocking error" label is redundant when hook provides complete disclosure #21504

@kitaekatt

Description

@kitaekatt

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions