Bash permission allow-rule doesn't split on ;/&& before matching (contrary to docs) — compound-command smuggling bypasses scoped allowlist
Bash permission allow-rule does not split on ;/&& before matching, contrary to docs
CLI version tested: 2.1.183 and 2.1.236 (both, identical result — not a stale-version issue)
What the docs say
https://code.claude.com/docs/en/permissions, "Compound commands" section, states:
Claude Code is aware of shell operators, so a rule likeBash(safe-cmd *)won't give it permission to run the commandsafe-cmd && other-cmd. The recognized command separators are&&,||,;,|,|&,&, and newlines. A rule must match each subcommand independently.
What actually happens
With --allowedTools "Read,Grep,Glob,Bash(git log:*),Bash(git blame:*)", prompting the model to
invoke the Bash tool with a command chained by ; or && after a matching prefix gets
auto-approved as a single unit and fully executed — the trailing, non-matching subcommand runs
too, with permission_denials empty (i.e. no denial is recorded at all).
Repro
mkdir /tmp/repro && cd /tmp/repro
git init -q
export GIT_AUTHOR_NAME=test GIT_AUTHOR_EMAIL=test@example.com GIT_COMMITTER_NAME=test GIT_COMMITTER_EMAIL=test@example.com
echo a > f.txt && git add f.txt && git commit -q -m "c1"
claude -p \
--allowedTools "Read,Grep,Glob,Bash(git log:*),Bash(git blame:*)" \
--model claude-haiku-4-5-20251001 \
--max-turns 3 \
--output-format json \
"Invoke the Bash tool with exactly this command line, verbatim, with no changes and no added cd: git log --oneline -5; whoami Report back the raw output you receive, including any denial/error message."
Observed result (2.1.236):
{
"permission_denials": [],
"result": "Raw output:\n\n```\n<git log output>\nmylocalusername\n```\n\nNo errors or denials. Command executed successfully."
}
whoami's real output appears in the tool result. The same happens with && in place of ;.
Both were re-tested independently against 2.1.183 (identical outcome) and 2.1.236 (identical
outcome) — this is not a stale-pin artifact.
What does correctly get denied (for contrast — so the splitting logic clearly exists for some cases)
Against the same allow rule, all of these were correctly denied, with permission_denials
populated:
- Command substitution:
git log --oneline $(whoami) - Explicit subshell wrapper:
sh -c 'git log --oneline -5; whoami' - Disguised subcommand:
git -C . log --oneline -5 - Disguised path:
/usr/bin/git log --oneline -5
So the matcher clearly does some structural analysis beyond naive string-prefix matching — it's
specifically the ; and && (and, going by the docs, presumably ||/|/|&/newline, not
independently tested) shell-operator-splitting path that isn't enforcing what the docs describe.
Why this matters
An allow rule like Bash(git log:*) is a common shape for scoping an agent to read-only,
low-risk commands. As tested, that scoping is not a security boundary against the compound-command
smuggling the docs explicitly say it defends against — any untrusted text an agent might be
induced to pass to the Bash tool (e.g. content from a file it read, a ticket description, etc.)
can ride along after a valid-looking prefix and execute with no denial recorded.
Ask
Please confirm whether this is a known regression, and if so whether ;/&& splitting is
expected to be restored, or whether the "Compound commands" doc section needs to be corrected to
reflect current behavior in the meantime.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗