PreToolUse hook with Bash matcher silently not invoked (UserPromptSubmit + PostToolUse hooks fire fine)

Resolved 💬 4 comments Opened Apr 24, 2026 by longhornrumble Closed Jun 20, 2026

Summary

A PreToolUse hook configured in ~/.claude/settings.json with matcher: "Bash" is not invoked on real Bash tool calls (including git commit), even though the same configuration's UserPromptSubmit and PostToolUse hooks fire correctly.

Manual invocation of the hook script with the exact same JSON payload the runtime would pass produces the expected behavior (block with exit 2). So the hook script is correct; the runtime's invocation is the gap.

Environment

  • Claude Code CLI (whichever version was current 2026-04-23/24 in this user's install — running claude --version would confirm)
  • macOS Darwin 24.6.0
  • Hook script at ~/.claude/hooks/pre-commit-gate.sh (executable, valid shebang #!/usr/bin/env bash)

Configuration

~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-commit-gate.sh"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/karpathy-reinjection.sh"
          }
        ]
      }
    ]
  }
}

A separate project-local .claude/settings.json defines a PostToolUse Bash hook (with if: "Bash(git push:*)" clause) that fires on every Bash tool call.

Reproduction

  1. Add a debug write to the top of ~/.claude/hooks/pre-commit-gate.sh:

``bash
{ printf '%s\n' "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] hook fired pid=$$ ppid=$PPID cwd=$PWD"; } >> /tmp/hook-debug.log 2>/dev/null || true
``

  1. Have Claude run a Bash tool call that contains git commit -m "test".
  2. Observe /tmp/hook-debug.log is never created — hook was not invoked.
  3. Manually pipe the equivalent payload into the hook:

``bash
echo '{"tool_name":"Bash","tool_input":{"command":"cd /repo/path && git commit -m test"}}' | ~/.claude/hooks/pre-commit-gate.sh
echo "exit=$?"
``
→ exit 2, prints "Staged changes have drifted from the last verified state" message — hook itself works correctly.

Expected

PreToolUse Bash hook should fire on every Bash tool call, mirroring how PostToolUse Bash hook fires.

Actual

Only UserPromptSubmit and PostToolUse hooks fire. PreToolUse Bash hook is silently inert. No error message in any logs visible to the user.

Impact

Skill-based commit gates (e.g., verify-before-commit skill that writes a per-repo marker the hook validates at commit time) are silently bypassed. Users believe their gate is enforced; in reality it isn't. CI catches the actual issues but the local pre-commit signal is lost.

Workaround

Manually invoke the gate skill before each commit. Awkward; defeats the purpose of an automated hook.

Notes

  • Tilde expansion (~/...) works for the other hook types (UserPromptSubmit script also uses ~).
  • Tested with both ~/... and absolute path for command: — neither caused the PreToolUse hook to fire.
  • The hook script returns exit 0 for non-git commit Bash calls (immediate pass-through), so even if it were firing on every Bash call, it would only block actual git commit attempts.

Asks

  1. Confirm whether PreToolUse + Bash matcher combination is supported in current Claude Code.
  2. If supported, identify the runtime path that's skipping invocation.
  3. If unsupported / deprecated, document the migration path (e.g., is Bash(git commit:*) the new matcher syntax?).

View original on GitHub ↗

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