PreToolUse hooks with exit code 2 do not block tool execution
Description
PreToolUse hooks that exit with code 2 are not blocking tool execution as documented. The hook correctly exits with code 2 and writes to stderr, but Claude Code proceeds with the Bash command anyway.
Steps to Reproduce
- Create a PreToolUse hook for Bash commands in
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/pre_git_checks.py",
"timeout": 300
}
]
}
]
}
}
- Create a hook script that:
- Detects git commit commands
- Runs validation checks
- Writes to stderr on failure
- Exits with code 2
- Stage a file that should fail validation
- Ask Claude Code to commit the changes
Expected Behavior
Per the documentation:
Exit code 2: Block the action. With exit code 2, only stderr content is used for the blocking message
The tool execution (git commit) should be blocked and the stderr message should be displayed.
Actual Behavior
The hook exits with code 2 and writes to stderr, but Claude Code ignores this and proceeds with the git commit.
Debug log from the hook shows:
[2025-12-11T22:00:52.058656] pre_git_checks: starting static analysis checks
[2025-12-11T22:00:52.067193] git diff --cached returned 1 files: ['test/claude/test_lint_fail.py']...
[2025-12-11T22:00:52.067269] Found 1 changed files: ['test/claude/test_lint_fail.py']
[2025-12-11T22:00:52.067702] Lint disable check FAILED - exiting with code 2
The hook wrote LINT DISABLE CHECK FAILED to stderr and exited with code 2, but the commit still succeeded.
Hook Code Snippet
if not run_lint_disable_check(changed_files):
log_debug("Lint disable check FAILED - exiting with code 2")
print("LINT DISABLE CHECK FAILED", file=sys.stderr)
sys.exit(2)
Environment
- macOS (Darwin 25.1.0)
- Claude Code CLI (latest as of 2025-12-11)
- Python 3.13
Impact
This bug means PreToolUse hooks cannot reliably prevent unwanted operations. In our case, we have a hook that runs static analysis (pylint, mypy) before git commits, but commits proceed even when checks fail.
Additional Context
- Multiple hooks are configured for the Bash matcher
- The hook correctly receives stdin JSON with the command
- The hook correctly parses and identifies git commit commands
- All exit(2) paths write to stderr before exiting
- The hook has been verified to work correctly in isolation
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗