[BUG] PreToolUse hook "if" permission-rule matcher fails open on Bash commands containing a `for` loop

Status Open
Reported on v2.1.216
Maintainer reply None cached
Activity 1 comment · opened Jul 21, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

A PreToolUse hook scoped to the Bash matcher with an "if" field (e.g.
"if": "Bash(git commit *)") fires on ANY Bash command that contains a
for ... do ... done loop, regardless of whether the command matches
the hook's pattern at all. The "if" filter is documented as
best-effort and "fails open" when it can't parse a command — and it
can't parse for...do...done loops, so any such command trips every
if-gated hook on that matcher, silently, with no error or warning.

What Should Happen?

The hook should only fire when tool_input.command actually matches the
configured "if" pattern (e.g. "Bash(git commit *)"). A for loop with
no relation to that pattern should not trigger it.

Error Messages/Logs

None — this is a silent behavior, not a crash or visible error. The
only symptom is the hook's own output/side effect (e.g. a block
message, or a confirmation prompt) appearing on a command it should
have ignored.

Steps to Reproduce

  1. Create .claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git commit *)",
            "command": "echo HOOK_FIRED >&2; exit 0"
          }
        ]
      }
    ]
  }
}
  1. Run this Bash command, which has nothing to do with git commit:
for i in 1 2 3; do echo "num $i"; done
  1. Observe: HOOK_FIRED is printed to stderr even though the command

doesn't match "Bash(git commit *)".

Isolation — same hook, each tested as its own isolated command:

| Command | Hook fires? |
|---|---|
| for i in 1 2 3; do echo "num $i"; done | Yes |
| while true; do echo x; break; done | No |
| if true; then echo x; fi | No |
| echo "not done yet, please do this" (bare "do"/"done" words) | No |

So the trigger is the for construct specifically, not the bare
keywords "do"/"done", not while...do...done, not if...then...fi.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

N/A

Claude Code Version

2.1.216 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

The hooks docs (https://code.claude.com/docs/en/hooks) state the "if"
field's Bash-command matching is best-effort and explicitly "fails
open, running your hook regardless of pattern, when the Bash command
can't be parsed." This looks like the same underlying weak point as
two other reports against the same static bash-command parser:

  • #21845 (closed "not planned") shows the parser splintering a for

loop into literal fragments ("Bash(for i in {1..15})", "Bash(do)",
"Bash(if ...)", etc.) instead of tokenizing it as one compound
statement, via the permission allow-list-capture path.

  • #43713 (open) shows the same parser bailing on $VAR, $(), {a,b,c},

and other expansions under autoAllowBashIfSandboxed, falling back to
an "ask" prompt instead of respecting the actual setting.

Different features (if-gated hooks here, allow-list capture in #21845,
sandboxed auto-allow in #43713), same apparent cause: when this parser
can't handle a construct, each feature's fallback is "act as if
nothing was recognized" rather than surfacing the parse failure — for
if-gated hooks specifically, that fallback is "run the hook anyway,"
which silently defeats the pattern scoping.

For hooks used as guardrails (e.g. blocking git push on main,
gating a merge behind confirmation), this means any session that
happens to use a for loop for something unrelated gets hit with
every if-gated hook on that matcher at once.

Related: #21845 (closed, not planned), #43713 (open) — same underlying
parser gap, different symptoms.

Reproduced from Claude Code on the web (claude.ai/code), not a local
CLI session — the PreToolUse hook and settings.json described above
were live, committed project config (not a synthetic test), so this
isn't specific to a local terminal setup.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗