PreToolUse hooks returning exit 2 + deny JSON do not block tool execution

Resolved 💬 4 comments Opened Apr 4, 2026 by mrrobotke Closed Apr 17, 2026

Bug Description

PreToolUse hooks that return permissionDecision: "deny" with exit code 2 are not blocking tool execution. The hooks execute (scripts run), but the platform ignores the deny decision and proceeds with the tool call.

Reproduction

1. Hook script (~/.claude/hooks/git-security-enforcer.sh)

#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
[[ -z "$command" ]] && exit 0

# Block git checkout
if echo "$command" | grep -qE "(^|[;&|[:space:]])git[[:space:]]+checkout([[:space:]]|$)"; then
  cat >&2 <<MSG
Git checkout blocked.
MSG
  cat <<JSON
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "git checkout blocked"
  }
}
JSON
  exit 2
fi
exit 0

2. Hook registration in ~/.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/git-security-enforcer.sh"
          }
        ]
      }
    ]
  }
}

3. Manual verification that the script works

echo '{"tool_name":"Bash","tool_input":{"command":"git checkout some-branch"}}' | bash ~/.claude/hooks/git-security-enforcer.sh
# Output: deny JSON on stdout, error message on stderr, exit code 2

4. Actual behavior in Claude Code

When Claude runs git checkout some-branch via the Bash tool:

  • Expected: Tool call blocked, deny message shown
  • Actual: Switched to branch 'some-branch' — checkout succeeds, hook deny ignored

5. Same issue with Edit/Write hooks

A second hook (prevent-main-branch-edits.sh) registered on Write|Edit matcher with the identical deny pattern also fails to block. The Edit tool proceeds and modifies the file.

Environment

  • Claude Code version: 2.1.87
  • Platform: macOS (Darwin 25.1.0)
  • Shell: zsh
  • Permission modes tested: bypassPermissions, acceptEdits — same behavior on both
  • Other hook types work: UserPromptSubmit hooks fire and inject context correctly. Stop hooks fire and block. Only PreToolUse deny decisions are ignored.

Key Observations

  1. UserPromptSubmit hooks execute and their output appears in system reminders — working
  2. Stop hooks execute and block stopping when they return non-zero — working
  3. PreToolUse hooks execute (the script runs, stderr output sometimes visible) but permissionDecision: "deny" is ignored — broken
  4. The issue is consistent across Bash, Edit, and Write tool matchers
  5. The issue persists regardless of defaultMode setting (bypassPermissions or acceptEdits)

Impact

Users relying on PreToolUse hooks for safety guardrails (blocking destructive git commands, preventing edits to protected files, enforcing worktree isolation) have no actual enforcement. The hooks provide a false sense of security.

View original on GitHub ↗

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