PreToolUse hook `if` filter false-positives on complex Bash commands
Environment
- Claude Code CLI:
2.1.121 - OS: Linux 6.18.26 (NixOS)
Summary
A PreToolUse hook with if: "Bash(git *)" fires on Bash commands that contain no git token anywhere. The schema (json.schemastore.org/claude-code-settings.json) and the hooks guide both describe if as a permission-rule-syntax filter that only triggers when the rule matches the tool input. In practice, complex Bash commands (nested command substitution, multi-; chains with backgrounding) trip the filter regardless.
The same behavior also appears to affect permissions.deny with the same rule, suggesting both share an underlying Bash sub-command parser that fails permissive into "rule matches" when it can't fully decompose the command.
Reproducer
~/.claude/settings.json:
\\\json\
{
\"hooks\": {
\"PreToolUse\": [{
\"matcher\": \"Bash\",
\"hooks\": [{
\"type\": \"command\",
\"if\": \"Bash(git *)\",
\"command\": \"printf '%s' '{\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PreToolUse\\\",\\\"permissionDecision\\\":\\\"deny\\\",\\\"permissionDecisionReason\\\":\\\"if-filter matched\\\"}}'\"
}]
}]
}
}
\\
Start a session and have Claude run each command below as a Bash tool call:
- \
ls /tmp\— runs (correct: no git, \if\does not match) - \
ls /tmp && echo done && date\— runs (correct) - \
date; ls /tmp 2>&1 | head\— runs (correct) - \
readlink -f \$(which sh); ls -la \$(dirname \$(readlink -f \$(which sh)))/../lib 2>/dev/null | head\— blocked by hook - \
ip -V; man ls 2>/dev/null | grep -A2 -i json | head -20; ls / & PID=\$!; sleep 1; kill \$PID 2>/dev/null\— blocked by hook
Neither (4) nor (5) contains the string \git\ (or even the substring \gi\). The \if: \"Bash(git *)\"\ filter should not match. The hook fires anyway and the deny response is returned.
Expected
\if\ only triggers when the command genuinely matches the rule, per its documented semantics — i.e. some sub-command of the tool input starts with \git \ (or is \git\ with no args).
Actual
The hook fires on commands containing nested \\$(...)\ command substitution or \;\ + \&\ backgrounding patterns, even when \git\ does not appear in the command at all.
Hypothesis
The Bash sub-command parser used to evaluate \Bash(...)\ rules cannot fully decompose certain complex commands and falls back conservatively to \"rule matches\" when uncertain. That is defensible for \permissions.deny\ (fail-closed on security-relevant rules), but is surprising and undocumented for \if\, where the user's intent is a precise filter, not a security boundary.
Workaround
Replace the \if\-filtered hook with a hook script that inspects \tool_input.command\ from stdin and emits the deny JSON only when the command actually invokes \git\.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗