[FEATURE] Add informational exit code (exit 3) for hooks that intentionally block tool calls

Resolved 💬 3 comments Opened Mar 24, 2026 by sharonyb Closed Mar 28, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

When a PreToolUse or Stop hook exits with code 2 to intentionally block or redirect a tool call, the output is displayed as:

Error: PreToolUse:Bash hook error: [hook-path]: <message>

Exit code 2 is the documented mechanism for hooks to deliberately pause or redirect tool calls — it's not an error. The "Error" framing:

  • Confuses users watching the output ("is something broken?")
  • May cause the model to treat the message as a failure rather than an instruction (see #24327 where Claude stops instead of acting on the feedback)
  • Plugins that use hooks for workflow control (e.g., "pause commit, run tests first") have no way to communicate a non-error block

This is a known pain point across multiple issues: #12667, #34600, #17088, #34713 — all reporting the same root cause from different hook types.

Proposed Solution

Add a new exit code (exit 3) with the same blocking behavior as exit 2, but displayed without the "Error" prefix:

│ Exit Code │ Behavior │ Display │
│ 0 │ Allow tool call │ (silent) │
│ 2 │ Block + show stderr │ Error: PreToolUse:Bash hook error: [...] (unchanged) │
│ 3 (new) │ Block + show stderr │ Hook: [...] or just the message (informational, no "Error") │

This is fully backwards compatible — existing hooks using exit 2 continue to work identically. Plugins that want a non-alarming block message opt in to exit 3.

Alternative Solutions

  1. Change exit 2 display — remove "Error" prefix from all exit 2 output. Simpler but breaks the expectations of hooks that genuinely use exit 2 for error reporting.
  2. Structured JSON output — hooks return JSON with a severity field (error vs info). More flexible but higher complexity for hook authors and a breaking change to the hook

protocol.

  1. Do nothing — plugins work around it by adding "(this is expected, not an error)" to their messages, which is what we do today.

Priority

High - Significant impact on productivity

Feature Category

CLI commands and flags

Use Case Example

Our plugin (https://github.com/earlyai/earlyai-claude-plugins) uses a PreToolUse hook to intercept git commit and ensure test coverage before committing. The block is
intentional and expected — the hook message says "commit paused (this is expected, not an error)" and provides workflow steps for Claude to follow.

With exit 3, the display would change from:
Error: PreToolUse:Bash hook error: [...] Early Unit Testing — commit paused...
To:
Hook: [...] Early Unit Testing — commit paused...

This correctly signals to both the user and the model that this is a workflow instruction, not a failure.

Additional Context

Related issues reporting the same underlying problem across different hook types:

  • #12667 — Stop hook displays 'error' for intentional blocking behavior
  • #34600 — Stop hook with exit code 2 displays as 'hook error' instead of feedback
  • #24327 — PreToolUse exit code 2 causes Claude to stop instead of acting on feedback
  • #17088 — PreToolUse shows 'error' label even for successful (exit 0) runs
  • #34713 — False "Hook Error" labels cause Claude to prematurely end turns

A new exit code would address all of these without breaking existing behavior.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗