PreToolUse hook `if` filter false-positives on complex Bash commands

Resolved 💬 2 comments Opened May 28, 2026 by arianvp Closed Jun 29, 2026

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:

  1. \ls /tmp\ — runs (correct: no git, \if\ does not match)
  2. \ls /tmp && echo done && date\ — runs (correct)
  3. \date; ls /tmp 2>&1 | head\ — runs (correct)
  4. \readlink -f \$(which sh); ls -la \$(dirname \$(readlink -f \$(which sh)))/../lib 2>/dev/null | head\blocked by hook
  5. \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\.

View original on GitHub ↗

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