Project-level permissions replace global permissions instead of merging
Status Open
Maintainer reply None cached
Workaround ✓ Mentioned in thread ↓
Activity 14 comments · opened Jan 9, 2026
Description
When a project has a local .claude/settings.local.json file with a permissions block, it appears to replace the global permissions from ~/.claude/settings.local.json rather than merging with them.
Expected Behavior
Global and local permissions should merge:
- Global allow:
["mcp__Ref__ref_search_documentation", "mcp__Ref__ref_read_url", "WebSearch", ...] - Local allow:
["mcp__expo-mcp__search_documentation"] - Effective allow: Union of both lists
Actual Behavior
Local permissions replace global:
- Tools allowed globally (e.g.,
mcp__Ref__ref_read_url) still prompt for permission when a localpermissionsblock exists but doesn't include them - Users must duplicate all global permissions in every project's local settings
Steps to Reproduce
- Add
"mcp__Ref__ref_read_url"to global~/.claude/settings.local.jsonpermissions allow list - Create a project with
.claude/settings.local.jsonthat has apermissionsblock but doesn't include that tool - Try to use the Ref tool - it will prompt for permission despite being globally allowed
Impact
This defeats the purpose of global settings. Users expect global permissions to apply everywhere unless explicitly overridden locally.
Suggested Fix
Merge permission arrays (union for allow, union for deny) rather than replacing them entirely when local settings exist.
---
🤖 Generated with Claude Code
14 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
I'm finding the same issue
same here.
Official Claude Code documentation mentions there's "merge" of the settings.
https://code.claude.com/docs/en/settings
Inheritance: Settings are merged, with more specific settings adding to or overriding broader ones
So the documentation must be adjusted or allowing it correctly!
I'm experiencing the same issue, but it's not limited to just permissions - configuration is also being completely overridden instead of merged.
Setup:
Expected behavior:
Actual behavior:
Workaround:
I have to duplicate the configuration in every project's , which defeats the purpose of having global defaults.
This issue started happening after updating Claude Code with
curl -fsSL https://claude.ai/install.sh | bash. The previous version worked fine with setting inheritance.It seems like any setting field (not just permissions) gets completely replaced instead of merged when a project config file exists. This is a significant regression in usability.
Try renaming your "settings.local.json" to "settings.json".
Details
I hit this too. But I notice you're talking about a file called settings.local.json. The documentation talks about ~/.claude/settings.json (no ".local."). Why did you create a "settings.local.json" file there?
In my case, I think what happened is that moved a project-level "settings.local.json" file that claude had created, to my ~/.claude/. The claude CLI's behavior - of creating a settings.local.json in whatever subdirectory I happen to be working in - is a poor way of saving my preferences (I work on multiple projects, using multiple machines).
I've been experimenting with various scenarios and found that merging works for both "allow" and "deny" permissions. Maybe some other combinations still don't work. But since I stopped using "settings.local.json" I haven't hit a repro (yet).
This is insanely annoying. I've added every freaking permission into allow in ~/.claude/settings.json, I have 'accept edits on', and it still asks my permission for everything. And when it adds its own permissions for the project it automatically adds them to settings.local.json, so everyone saying to rename this file to settings.json is just avoiding the issue. Is this a merging issue or just a crazy permissions bug?
and when i check /permissions - all the permissions i set globally in ~/../settings.json are there! Still asks so many permission questions.
I wasn't suggesting to rename the project-level settings.local.json. I was talking specifically about a user-level one - that is not supported, but is an easy situation to end up in. And, it is what this bug report is reporting.
Once I cleaned that up, and after testing very carefully, I realized I could no longer repro the reported problem. I say "very carefully" because there are other very similar, very annoying bugs/design problems here that have nothing to do with the merging behavior:
I set them up like this:
I use * to catch all variations. Maybe it works for some commands, but not all, because it asks me for permissions alot
all of these were added when you answer "Dont ask me again for this project"
....
**But I have this permission in my settings.json: "Bash(npx: )",*
So there should be no need for these:
Am I setting the catch all's wrong?
is it Bash(npx: *) or Bash(npx *) or Bash(npx*) or Bash(npx: **) ??We need better permission docs, way more examples, and even claude is unsure sometimes when you ask it.
Yes, almost every time I asked Claude to help, it first formed the theory that
":*"is invalid syntax. But that's a hallucination. (Claude did eventually help me understand the cases I investigated, with a lot of hand-holding).Bash(npx:*)works fine. But the problem is that Claude doesn't always emit a straightBash(npx ...)command. e.g. sometimes it doesBash(cd c:\myproject && npx ...), which doesn't match"Bash(npx:*)". For some commands it throws another "bash" in there - likeBash(bash foo). And there are probably more variations I haven't noticed/encountered yet.I think I fixed it by switching all the rules to this style:
"Bash(curl *)",
"Bash(diff *)",
"Bash(echo *)",
"Bash(file *)",
"Bash(find *)",
"Bash(gh *)",
"Bash(git *)",
"Bash(grep *)",
"Bash(head *)",
"Bash(kill *)",
"Bash(locate *)",
"Bash(ln *)",
"Bash(ls *)",
"Bash(lsof *)",
"Bash(mkdir *)",
"Bash(mudslide *)",
"Bash(mv ./**)",
"Bash(node *)",
"Bash(npm *)",
"Bash(npx *)",
I think using the ":" syntax is wrong or maybe i was doing it wrong without the space between the colon and the star.
Workaround: SessionStart hook that merges global permissions into each project.
<details>
<summary>~/.claude/hooks/sync-permissions.py</summary>
</details>
Hook config (
~/.claude/settings.json):Running on multiple hooks ensures permissions sync even if global settings change mid-session. Would also benefit from running on
SubagentStartonce that hook is fully supported (#19170).Workaround — hooks are additive, not replaceable:
Unlike permission rules (where project can replace global), hooks in
~/.claude/settings.jsonalways fire regardless of project-level settings. They're the one mechanism that truly provides global enforcement.Writes 8 safety hooks to user-level
~/.claude/settings.json. These fire for every project, every agent, every tool call — project-level settings can't disable them.Still happening — adding a current confirming repro on Windows:
~/.claude/settings.jsonpermissions.allowcontainsRead(//c/REPOS/**)..claude/settings.local.jsonhas its ownpermissions.allowblock listing unrelated rules (noReadrule).c:/REPOS/...fires a permission prompt despite the global rule covering it.Read(//c/REPOS/.../WebConsole/**)) into project.claude/settings.json, which becomes the permanent residue.Same symptom for
additionalDirectories: a directory present in globaladditionalDirectoriesis re-prompted when the project settings file has its ownadditionalDirectoriesarray.Net effect: any project that adds even one project-level permission rule effectively forfeits the entire global allow-list.
Environment: Windows 11 Enterprise, Claude Code VS Code extension.
Confirming this is still happening in the CLI (not just the desktop app) as of v2.1.206/v2.1.207 (2026-07-09/07-10).
Repro: global
~/.claude/settings.jsonhaspermissions.defaultMode: "bypassPermissions". Any project whose.claude/settings.jsonhas its ownpermissions.allowarray (auto-populated over time via "don't ask again" clicks) — with nodefaultModekey of its own — loses the global bypass mode entirely. Bash/Edit/etc. calls in that project prompt for approval again despite bypass being set globally.Audited 38 project-level settings.json files on one machine: 36 had a project-level
permissionsblock with nodefaultMode, all exposed to this. So this isn't an edge case — it's the default outcome of normal usage (clicking "don't ask again" once in a project is enough to trigger it).Workaround for now: explicitly set
"defaultMode": "bypassPermissions"in each project's.claude/settings.jsonalongside itsallowlist. That confirms the mechanism — it's not that the global default fails to apply in general, it's that presence of any project-levelpermissionsobject fully overrides rather than merges with the global one.+1 for the merge-not-replace fix suggested above.