PreToolUse hooks with exit code 2 do not block tool execution

Resolved 💬 3 comments Opened Dec 12, 2025 by jdrowne Closed Dec 12, 2025

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

  1. 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
          }
        ]
      }
    ]
  }
}
  1. Create a hook script that:
  • Detects git commit commands
  • Runs validation checks
  • Writes to stderr on failure
  • Exits with code 2
  1. Stage a file that should fail validation
  2. 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

View original on GitHub ↗

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