Bash permission rules are quote-blind: an operator inside a quoted argument defeats a matching rule
Summary
Bash(...) permission rules are matched against the raw command string without regard to shell quoting, so a shell operator inside a quoted argument (a > in an awk program, a && in a git commit -m message, a | in a grep pattern) defeats a rule that should match and forces a prompt. Conversely, a shell variable or $(...) anywhere in the command falls through the rule to the classifier, even when the variable is inert. The docs say Claude Code "is aware of shell operators" for compound commands, but the awareness stops at operators inside quotes.
Reproduction
With Bash(awk *) in permissions.allow (default permission mode):
awk '{print NR}' file # runs, no prompt
awk 'NR>=300 && NR<=430' file # prompts: "uses shell operators that require approval"
The second command has no shell operator. > and && are inside a single-quoted awk program and never reach the shell.
Same class:
git commit -m "Fix a && b" # prompts on the && inside the message
grep -n 'a|b' file # prompts on the | inside the pattern
gh pr create --body "$(cat x.md)" # falls through a matching Bash(gh pr create *) allow
Impact
In a repository with ~250 Bash rules we deleted Bash(awk *) outright because it could not be trusted to match, and we maintain a CLAUDE.md rule telling the model to rewrite awk as sed -n / cut to avoid the operator. Any rule whose command routinely takes a quoted program or free text (awk, sed, jq, grep -E, git commit -m, gh ... --body) has the same problem. The workaround we converged on is a PreToolUse hook with a hand-written shell tokenizer (about 400 lines) that decides these commands instead of the rules, which is a lot of machinery to replace a quote-aware tokenizer.
Expected
Rule matching should tokenize the command the way the shell will: a quoted argument is one word, and an operator inside it is data. A rule matching the command word should then match regardless of what its quoted arguments contain. Variable expansion is harder, but a plain $VAR (no substitution) in argument position should not by itself void a matching rule.
Environment
Claude Code 2.1.274, macOS (Darwin 25.2), default permission mode auto, seatbelt sandbox enabled.
Related
- #79759 asks for specificity-aware precedence between rules; this is a different gap (the match itself, not its precedence).
- The docs page
permissions.mdsection "What a Bash rule doesn't match" describes operator splitting but not quoting.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗