claude-code - 💡(How to fix) Fix PreToolUse hook `if` filter false-positives on complex Bash commands

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 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.

Root Cause

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.

Fix Action

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`.

RAW_BUFFERClick to expand / collapse

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`.

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