Permission system UX: compound command blocking, rule accumulation, undiscoverable Bash(*) fix
Summary
The permission system has a compounding UX problem that makes "autonomous" agent usage impractical for long-term users. After 3 months of daily use, I accumulated 150+ narrow permission rules in settings.local.json — and compound commands (cd /my/project && git status) still prompted for approval every time because they don't match the individually-allowed patterns.
This is not one bug — it's a system-level UX failure across permission matching, compound command handling, and discoverability.
The Experience
- Month 1: Click "Always allow" on every
git status,ls,python script.pyprompt. Accumulate 50+ rules. - Month 2: Still getting prompted on compound commands. Click "Allow" hundreds more times. 100+ rules now.
- Month 3: 150+ rules. Compound commands STILL prompt with security warnings like "bare repository attack prevention" — on my own project directory that I've been working in for months. Agent autonomy is fiction.
- Discovery: Learn about
Bash(*)from reading source code. Replace 150 rules with 8 broad wildcards. Problem solved instantly. This should have been offered on day 1.
The Four Compound Command Blockers
These fire constantly when working in your own trusted project:
- "Compound commands with cd and git require approval to prevent bare repository attacks"
- "Compound command contains cd with output redirection - manual approval required to prevent path resolution bypass"
- "Compound commands with cd and git require approval" (variant)
- General compound command security checks
These assume adversarial scenarios (cloning untrusted repos, path traversal) that don't apply to users working in their own project root. They cannot be disabled without discovering Bash(*).
Evidence: My settings.local.json Before Cleanup
150+ entries including:
- Every individual command:
Bash(python:*),Bash(git status:*),Bash(ls:*),Bash(cd:*),Bash(git log:*), etc. - One-off commands that got permanently saved:
Bash(RUN_OVERNIGHT_ANALYSIS.bat core), individualforloop fragments, multi-line Python heredocs with full escaped source code as permission patterns - Compound commands that were allowed once but the pattern was too specific to match again
Despite all of this, cd /project && git status still prompted.
The Fix (Trivial but Undiscoverable)
{
"permissions": {
"allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)", "Glob(*)", "Grep(*)"],
"ask": ["Bash(rm -rf:*)", "Bash(git push --force:*)", "Bash(git reset --hard:*)"]
}
}
8 rules replace 150+. Dangerous commands still prompt. Everything else just works. Real safety comes from hooks (RAM auditing, pre-commit validation), not permission prompts on ls.
Related Issues (13 reports, same root cause)
Compound command blocking:
- #28183 — Compound commands of individually-allowed commands still prompt
- #29491 — Compound bash commands should evaluate each command's permissions independently
- #20985 — Feature request for smart permission matching on compound shell commands
- #30435 — Feature request to suppress bash safety heuristic prompts via settings
- #30524 — Windows: unnecessary
cdprepended to commands triggers bare repository prompt
Permissions not sticking / wildcards broken:
- #27139 — Broad wildcard permissions in settings.local.json not respected
- #29400 — "Don't ask again" doesn't persist
- #11380 — Continually asks for permission after "always allow"
- #10629 — Asking permission too often
- #3428 —
Bash:*wildcard not working
Trust model:
- #29285 — Feature request to permanently trust a directory
- #28784 — Security:
Bash(cd:*)allows arbitrary execution via&&chaining
Token injection:
- #15742 — Multi-line bash
__NEW_LINE__token appears in saved permission patterns
Proposed Solutions
- Offer "trust this project" during first session — a first-class option that sets
Bash(*)+ dangerous-onlyasklist - Make
Bash(*)discoverable — surface it in/permissions, in the "Always allow" dialog, and in docs - Compound command permissions should decompose —
cd /path && git statusshould checkBash(cd:*)ANDBash(git status:*)individually, not treat the compound as a new unique pattern - Scope security checks to actual risk — bare repository attack protection should only apply when entering directories outside the project root, not when
cd-ing within your own working tree - Periodic permission cleanup prompt — after 50+ rules accumulate, suggest consolidation
Environment
- Claude Code v2.1.70
- Windows (MSYS2/Git Bash)
- Daily user since December 2025
- 11 custom agents, hooks for RAM safety / pre-commit / compaction recovery
5 Comments
I've been having this problem for many months. Seems super annoying and very obvious that should be fixed. I kind of expected it will be done pretty soon. Not sure what's taking people so long.
From my point of view, it's obvious that commands should be decomposed and matched to the allowed commands.
Just so you guys know, I'm afraid I had to switch to Codex and Gemini due to this issue :(
I'm gonna continue paying for Anthropic Max for a couple of months because I love you guys and I kind of despise OpenAI. Godspeed. Hope you get it fixed soon!
/tmp/gh-comment-body.txt
We ran into the deny-side of this same issue and built a PreToolUse hook that fixes it: PR #36645.
The root cause is identical to what you've described — CC evaluates the full compound string, so a dangerous command in a non-first segment never matches deny rules, just as your allowed patterns never match compound allow rules.
The hook decomposes on &&, ||, ;, and | before evaluating each segment.
Solution 3 in your list ("compound command permissions should decompose") is exactly the right fix for both sides.
We've cross-referenced this issue over in #37662 if the Anthropic team wants to track them together.
Reproduced with a slightly different shape: a single
&&chain of twoalready-allowed, non-read-only commands (not
cd/git).settings.json:
Command:
source .venv/bin/activate && python3 some_script.pyBoth halves individually match an allow rule (
Bash(source *)andBash(python3 *)), and I even added an explicit compound-shaped rule(
Bash(source * && python3 *)) hoping the literal&&in the patternwould help -- it doesn't, since the matcher splits on shell operators
before comparing, so a pattern that itself contains
&&never linesup with anything. Still prompts for the whole string every time.
Workaround in case it helps anyone hitting this thread: a PreToolUse
hook that re-splits the command on shell separators (quote-aware,
bails out on
$()/backticks) and checks each piece against a smallknown-safe prefix list, emitting
permissionDecision: allowonly whenevery piece matches. Deny/ask rules still apply independently per the
docs, so this can only skip a prompt that was already allow-listed,
never weaken an existing restriction.
Adding data from an enterprise fleet (~320 users) and a quick survey of how peer tools handle this, since it's directly relevant to the compound-command-blocking behavior described here.
Confirmed still non-configurable: the
cd <dir> && <command>family fires regardless ofpermissions.allowcoverage — not just forgit(the commonly-cited "bare repository attack" case) but also for output redirection and write operations after acd. No allowlist entry, wildcard, or settings flag suppresses it. This matches what's already reported in #30213 and #28183.How other AI coding tools handle the same underlying risk (untrusted local state — malicious hooks/config — picked up after changing into a directory):
.git/.agents/.codexread-only within an otherwise-writable sandbox root as a targeted mitigation, rather than relying on sandboxing alone. Their exec tool also takes a structuredworkdirparameter instead of shellcd, though by their own account the model doesn't always use it.None of these solve it by blocking the command's textual shape indefinitely with zero override, which is the current Claude Code behavior. The common thread is deciding trust once per directory (optionally re-validated on tampering) rather than re-litigating it on every command that happens to contain
cd. A per-directory trust mechanism (even something as simple as an enterprise-configurable "trusted repo roots" allowlist, invalidated if hooks/config under that root change) seems like it would resolve this without reopening the actual attack this check is meant to prevent.Happy to share more detail on the fleet-level friction numbers if useful.