claude-code - 💡(How to fix) Fix PostToolUse hook `if: Bash(<head> *)` matcher fires on commands that don't match

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

A PostToolUse hook with if: "Bash(git commit *)" fires on Bash commands that do not start with git commit. The if: clause is supposed to gate execution, but empirically the hook script runs for unrelated commands.

Root Cause

A PostToolUse hook with if: "Bash(git commit *)" fires on Bash commands that do not start with git commit. The if: clause is supposed to gate execution, but empirically the hook script runs for unrelated commands.

Fix Action

Workaround

Self-guard at the script head:

TRIMMED="${COMMAND#"${COMMAND%%[![:space:]]*}"}"
case "$TRIMMED" in
  "git commit"|"git commit "*) ;;
  *) exit 0 ;;
esac

This works but defeats the purpose of if: — every hook now has to re-implement matcher logic to be safe.

Code Example

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/test-trigger.sh",
            "if": "Bash(git commit *)"
          }
        ]
      }
    ]
  }
}

---

#!/bin/bash
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
echo "FIRED ON: $COMMAND" >> /tmp/hook-misfire.log

---

TRIMMED="${COMMAND#"${COMMAND%%[![:space:]]*}"}"
case "$TRIMMED" in
  "git commit"|"git commit "*) ;;
  *) exit 0 ;;
esac
RAW_BUFFERClick to expand / collapse

Summary

A PostToolUse hook with if: "Bash(git commit *)" fires on Bash commands that do not start with git commit. The if: clause is supposed to gate execution, but empirically the hook script runs for unrelated commands.

Repro

~/.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/test-trigger.sh",
            "if": "Bash(git commit *)"
          }
        ]
      }
    ]
  }
}

~/.claude/hooks/test-trigger.sh:

#!/bin/bash
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
echo "FIRED ON: $COMMAND" >> /tmp/hook-misfire.log

Then run any non-git commit Bash command via the agent — e.g.:

  • iis-nexus knowledge resolve 1234 --resolution disproven
  • git status
  • gh issue list

Each appears in /tmp/hook-misfire.log despite if: "Bash(git commit *)".

Expected

The if: clause should gate the hook to only fire when the executed command matches Bash(git commit *) — i.e. the command starts with git commit. Other Bash commands should not invoke the hook script at all.

Actual

The hook fires on every Bash command regardless of the command's content.

Impact

Hooks that produce hookSpecificOutput (e.g. UserPromptSubmit-style context injection on PostToolUse) end up injecting their context after every Bash command. In our case, a "code committed" reflection prompt was being injected after iis-nexus knowledge resolve, git status, and other non-commit commands, creating significant noise in the agent's context.

Workaround

Self-guard at the script head:

TRIMMED="${COMMAND#"${COMMAND%%[![:space:]]*}"}"
case "$TRIMMED" in
  "git commit"|"git commit "*) ;;
  *) exit 0 ;;
esac

This works but defeats the purpose of if: — every hook now has to re-implement matcher logic to be safe.

Environment

  • Claude Code 2.1.139
  • macOS 25.4.0 (Darwin)
  • Confirmed by 17/17 regression tests against a hardened script vs. the original

Suggestion

Either:

  1. Document if: clauses as performance hints only (not correctness gates), so authors know to self-guard.
  2. Fix the matcher so it actually gates the hook invocation.

Option 2 is preferable — option 1 makes the if: clause largely useless.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING