Permission system UX: compound command blocking, rule accumulation, undiscoverable Bash(*) fix

Status Open
Maintainer reply None cached
Activity 5 comments · opened Mar 6, 2026

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

  1. Month 1: Click "Always allow" on every git status, ls, python script.py prompt. Accumulate 50+ rules.
  2. Month 2: Still getting prompted on compound commands. Click "Allow" hundreds more times. 100+ rules now.
  3. 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.
  4. 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:

  1. "Compound commands with cd and git require approval to prevent bare repository attacks"
  2. "Compound command contains cd with output redirection - manual approval required to prevent path resolution bypass"
  3. "Compound commands with cd and git require approval" (variant)
  4. 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), individual for loop 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 cd prepended 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

  1. Offer "trust this project" during first session — a first-class option that sets Bash(*) + dangerous-only ask list
  2. Make Bash(*) discoverable — surface it in /permissions, in the "Always allow" dialog, and in docs
  3. Compound command permissions should decomposecd /path && git status should check Bash(cd:*) AND Bash(git status:*) individually, not treat the compound as a new unique pattern
  4. 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
  5. 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

View original on GitHub ↗

5 Comments

sldx · 5 months ago

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!

yurukusa · 5 months ago

/tmp/gh-comment-body.txt

Pro777 · 5 months ago

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.

ledyba · 2 months ago

Reproduced with a slightly different shape: a single && chain of two
already-allowed, non-read-only commands (not cd/git).

settings.json:

{
  "permissions": {
    "allow": [
      "Bash(source *)",
      "Bash(python3 *)",
      "Bash(source * && python3 *)"
    ]
  }
}

Command:
source .venv/bin/activate && python3 some_script.py

Both halves individually match an allow rule (Bash(source *) and
Bash(python3 *)), and I even added an explicit compound-shaped rule
(Bash(source * && python3 *)) hoping the literal && in the pattern
would help -- it doesn't, since the matcher splits on shell operators
before comparing, so a pattern that itself contains && never lines
up 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 small
known-safe prefix list, emitting permissionDecision: allow only when
every 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.

johnib · 1 month ago

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 of permissions.allow coverage — not just for git (the commonly-cited "bare repository attack" case) but also for output redirection and write operations after a cd. 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):

  • VS Code Workspace Trust: trust is a one-time, per-folder decision. Untrusted folders run in Restricted Mode (execution disabled); once trusted, there's no further per-command prompting. Trust can also be extended to a parent folder + all subfolders in one action.
  • direnv / rvm: same per-directory trust idea, but content-hash-gated — trust is granted once, and automatically invalidated (re-prompting) only if the directory's config file actually changes post-approval. Tampering re-triggers review; routine re-use doesn't.
  • OpenAI Codex CLI: relies on an OS-enforced sandbox (Seatbelt/bubblewrap+seccomp+Landlock) to contain blast radius, explicitly to avoid per-command prompting — their docs state "the sandbox reduces approval fatigue... instead of asking you to confirm every low-risk command." They additionally keep .git/.agents/.codex read-only within an otherwise-writable sandbox root as a targeted mitigation, rather than relying on sandboxing alone. Their exec tool also takes a structured workdir parameter instead of shell cd, 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.