[BUG] PreToolUse hook `if: "Bash(...)"` still false-positives on `$()` command substitution in 2.1.202

Open 💬 0 comments Opened Jul 7, 2026 by stefanobaghino

Environment

  • Claude Code CLI: 2.1.202
  • OS: macOS (Darwin 25.5.0)

Summary

A PreToolUse hook with if: "Bash(git commit*)" still fires on Bash commands that contain command substitution $(...) but no git token anywhere — e.g. echo $(date). The v2.1.163 changelog claims this class of false-positive was fixed:

Fixed hook if: "Bash(...)" conditions firing on every Bash command containing $() or $VAR; the pattern now matches against commands inside subshells and backticks too

On 2.1.202 the fix only holds for the $VAR case. Plain variable references are now correctly parsed and skipped, but command substitution $(...) still trips the filter into a spurious match. This is the same underlying behavior reported in #63066 (closed as not planned by the inactivity bot, not fixed) and is what makes the if semantics in #65501 (docs) observably wrong for $().

Minimal reproducer

~/.claude/settings.json (single hook, no external script — writes a marker file only when the filter matches):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git commit*)",
            "command": "touch /tmp/hook-fired"
          }
        ]
      }
    ]
  }
}

Start a session and have Claude run each command as a Bash tool call, deleting the marker before each:

| # | Command | /tmp/hook-fired created? | Correct? |
|---|---------|---------------------------|----------|
| 1 | echo hi | no | ✅ correct — no git |
| 2 | echo "$HOME" | no | ✅ correct — $VAR, fixed in 2.1.163 |
| 3 | echo $(date) | yes | ❌ bug — no git token; $() trips the filter |
| 4 | git commit -m x | yes | ✅ correct — genuine match |

Expected

if: "Bash(git commit*)" matches only when a parsed subcommand actually starts with git commit. echo $(date) has one inner subcommand, date, which does not match, so the hook should be skipped — exactly as the 2.1.163 changelog says (“matches against commands inside subshells and backticks too”).

Actual

The hook fires on any command containing $(...) (and backticks) when the if pattern is more specific than a bare command name, regardless of whether git appears at all.

Evidence (debug log, --debug, this exact behavior on 2.1.202)

The auto-mode classifier logs the command each hook decision applies to. For echo $(date), the Bash(git commit*) hook fired with no skip line:

[auto-mode] new action being classified: {"Bash":"echo $(date)", ...}
...
[DEBUG] Hook PreToolUse (.../hygiene.sh commit) provided additionalContext (1132 chars)

For commands with a plain $VAR (f="..."; grep ... "$f"), the same hook was correctly skipped:

[DEBUG] Skipping hook due to if condition "Bash(git commit*)" not matching

The Skipping ... not matching line appears for every $VAR-only command and never for the $(...) command.

Impact

The if filter is documented as a precise filter, not a security boundary, so failing permissive on $() is surprising. In practice it fires hooks (and, for permissionDecision hooks, gates tool calls) on a large fraction of ordinary commands, since $(...) is extremely common. It is especially bad for hooks that inspect PR/commit bodies, whose content routinely contains $(...) and backticks in code snippets.

Workaround

Drop if and inspect tool_input.command from the hook's stdin, matching git/gh prefixes in the script itself.

Related

  • #63066 — same behavior on 2.1.121, closed as not planned by the inactivity bot (reproducer #4 is the nested-$() case).
  • #65501 — docs for if: "Bash(...)" matching; the $() example there is exactly the case still misbehaving.
  • v2.1.163 changelog entry claiming this was fixed.

View original on GitHub ↗