Where a setting comes from
In this lesson we find out why a setting seems ignored. You add a rule to a settings file and the agent prompts anyway. Or a command you never approved runs without a prompt. The setting wasn’t lost. Another file set the same thing, and a fixed order decided which one counts. We list the files in that order, work one example where four of them disagree and derive the outcome entry by entry, and then sort out what a team commits and what each person keeps to themselves.
The file names, the order and the merging rules here are Claude Code’s, checked against the vendor’s documentation on the date at the end of the lesson [1]. Other coding agents layer their settings too, under other file names and in a different order. The way of reading a layered configuration transfers, and the exact files may not.
The layers, highest first
Section titled “The layers, highest first”Settings layering is the fixed order in which the places a setting can come from override each other. Claude Code reads four settings files, and an organization can add a fifth source above them. When the same key is set in more than one place, the value from the highest level that sets it applies [1].
| Level | Where it comes from | Who it affects |
|---|---|---|
| 1. Managed | A managed-settings.json file, a device policy, or the organization’s console | Everyone the organization deploys it to |
| 2. Command line | Flags on the claude command, for one session | You, this session |
| 3. Project local | .claude/settings.local.json | You, in this one project |
| 4. Shared project | .claude/settings.json, committed to the repository | Everyone who clones the project |
| 5. User | ~/.claude/settings.json | You, in every project on this machine |
The table is all you need for a key that holds one value.
permissions.defaultMode, the mode a session starts in, is such a key.
If your user file says acceptEdits and the team’s committed file says
default, your sessions in that project start in default, because
shared project sits above user. A --permission-mode flag overrides both
for that one session [1].
A key that holds a list works differently, and the permission rules are
lists. When permissions.allow, ask or deny appear in more than one
file, Claude Code combines the lists. Each file adds its entries, and no
file removes another file’s [1]. The merged lists are
then read the way the last lesson described: deny first, then ask,
then allow, and the first match decides. A deny rule in any file blocks
a tool call that an allow rule in any other file would have approved,
because the deny rules of every file are checked before the allow rules
of any [2]. So for a rule the question “which
layer decides?” has a different answer than for defaultMode: the layer that
holds the first matching rule in deny, ask, allow order, at whatever
level that’s at.
The command line adds rules the same way. --disallowedTools adds deny
rules for the session, --allowedTools adds allow rules for the session,
and a deny rule from any settings file still blocks a tool that
--allowedTools names [2]. The /permissions
dialog lists every rule from your settings files with the file each one
came from, so when an outcome surprises you, that dialog is where to look [2].
Order the layers
Section titled “Order the layers”A coding agent reads its settings from several places. When one key that holds a single value is set in more than one of them, the highest level that sets it wins.
- Settings the organization deploys
- Flags on the command that starts the session
- The uncommitted local file in the project
- The committed file in the project
- The file in your home directory
Which source can nobody on the team override, and which one applies to every project on your machine?
When the files disagree
Section titled “When the files disagree”Here is one project with four sources of permission settings. Your user
file allows the read-only git commands and starts every session in
acceptEdits. The team’s committed file allows the test command and
edits to todo.py, denies edits to todos.json and any git push, and
starts sessions in default. Your local file, which you wrote while
trying things out, allows every python3 command and git push. You
started this session with --disallowedTools "Bash(rm *)".
// ~/.claude/settings.json (user){ "permissions": { "defaultMode": "acceptEdits", "allow": ["Bash(git status *)", "Bash(git diff *)"] } }
// .claude/settings.json (shared project){ "permissions": { "defaultMode": "default", "allow": ["Bash(python3 -m unittest *)", "Edit(todo.py)"], "deny": ["Edit(todos.json)", "Bash(git push *)"] } }
// .claude/settings.local.json (project local){ "permissions": { "allow": ["Bash(python3 *)", "Bash(git push *)"] } }The program below lists the same four layers in LAYERS, highest
first, under the short names flag, local, project and user, and
derives the outcome for a list of tool calls. It is a model of
the two documented rules, the highest layer decides a one-value key and
lists merge and are read deny, ask, allow, with the same small matching
as the allowlists lesson. Claude Code itself also strips wrappers and
splits compound commands, and it matches paths as patterns. The model
leaves those out. The complete file is
site/examples/customizing-agents/settings-layering/effective.py in the
repository.
LAYERS = [ ("flag", {"deny": ["Bash(rm *)"]}), ("local", {"allow": ["Bash(python3 *)", "Bash(git push *)"]}), ("project", {"defaultMode": "default", "allow": [...], "deny": [...]}), ("user", {"defaultMode": "acceptEdits", "allow": [...]}),]
def decide(layers, action): for verdict in ("deny", "ask", "allow"): for name, permissions in layers: for rule in permissions.get(verdict, []): if matches(rule, action): return f"{verdict} ({name} {verdict} {rule})" mode, name = single_value(layers, "defaultMode") return f"prompt (no rule; mode {mode} from {name})"Work through the first two calls before you reveal the answer. The push
matches an allow rule in your local file and a deny rule in the committed
file. Deny is read first, from every file. The committed deny decides
before the local allow is checked. The python3 todo.py add command
matches no rule in the committed file, where the only python3 rule
needs -m unittest after it, and matches the wide Bash(python3 *) in
your local file. The local layer decides. Now derive the other three.
Derive the effective set
Section titled “Derive the effective set”A Python program models how a coding agent combines permission settings from four layers, listed highest first as flag, local, project and user. A single-value key takes the highest layer that sets it. Rule lists merge, and a tool call is checked against every layer's deny rules, then ask, then allow, first match wins. The user file allows git status and git diff and sets the mode acceptEdits. The project file allows the unittest command and edits to todo.py, denies edits to todos.json and git push, and sets the mode default. The local file allows every python3 command and git push. A session flag denies rm.
What does this print, one line per tool call?
ACTIONS = [ "Bash(git push origin main)", "Bash(python3 todo.py add milk)", "Bash(rm -rf build)", "Edit(todos.json)", "Bash(git commit -m fix)",]
for action in ACTIONS: print(f"{action}: {decide(LAYERS, action)}")Bash(git push origin main): deny (project deny Bash(git push *)) Bash(python3 todo.py add milk): allow (local allow Bash(python3 *)) Bash(rm -rf build): deny (flag deny Bash(rm *)) Edit(todos.json): deny (project deny Edit(todos.json)) Bash(git commit -m fix): prompt (no rule; mode default from project)
Output verified in CI from site/examples/customizing-agents/settings-layering/effective.py.
For each call, which merged list is checked first, and which layer holds the first rule that matches? For the last call, which layer sets defaultMode highest?
Read the last line again. The commit matches no rule anywhere. The
rules say nothing, and the mode decides whether it prompts. The mode is
one value, the committed file sets it, and the committed file is
above your user file, so your acceptEdits doesn’t apply in this
project. That’s the setting that “seems ignored”. It was set, and a
higher layer set it too.
What belongs in which layer
Section titled “What belongs in which layer”The order is also a division of labor. A team puts what everyone needs
in the committed .claude/settings.json: the deny rules for files that
hold secrets, the allow rules for the test command and the build, the
hooks, and the environment variables the project needs. Everyone who
clones the repository gets them, and a change to them goes through the
same review as a change to the code [1]. Its allow
rules and its extra directories grant capability, so Claude Code applies them only after you
accept the trust dialog for that folder, while its deny and ask rules
apply right away [2]. And auto and
bypassPermissions as its defaultMode don’t take effect from a project
or local file, so a repository can’t start every clone in a hands-off
mode [1].
Your user file, ~/.claude/settings.json, holds what you want in every
project, such as your default model and the permission rules you want
everywhere. The project local file, .claude/settings.local.json,
holds your exceptions for one project and the rules you’re testing before
you propose them to the team. When you answer a Bash prompt with “Yes,
and don’t ask again”, Claude Code saves an allow rule in that local file,
and the first time it creates the file in a repository it adds the file
to your global git excludes. If you create the file by hand, add it to
.gitignore yourself [1].
The local file is above the committed one, so for a one-value key such
as the model or defaultMode, your local entry wins in your sessions and
changes nothing for your teammates. For rules the local file can only add
entries. A local allow rule doesn’t override an ask rule in the committed
file or in managed settings, so if a saved approval still prompts, that
is why [1]. Managed settings, when your organization
uses them, sit above everything, and no file of yours and no flag
overrides a managed permission rule [2].
Which layer does it belong in?
Section titled “Which layer does it belong in?”A coding agent reads settings from a file in the user's home directory that applies to every project, a committed file in the project that everyone who clones it gets, an uncommitted local file in the project for one person, and flags on the command that starts a session.
Who should the setting affect, and for how long?
A setting that seems ignored
Section titled “A setting that seems ignored”Claude Code reads its settings from five levels, and a key with one value takes the highest level that sets it.
Your user file sets defaultMode to acceptEdits, and sessions in one
project still start in default. What do you do?
Which levels are higher than the user file?
An allow here, a deny there
Section titled “An allow here, a deny there”In Claude Code the permission rule lists merge across the settings files, and the merged lists are read deny, then ask, then allow.
Your user file allows Bash(git push *), and the committed project file
denies it. Which two of these are true?
Does one file remove another's rules, and which list is read first?
Which file holds it?
Section titled “Which file holds it?”A coding agent reads settings from a user file in the home directory for every project, a committed project file for everyone who clones it, an uncommitted local project file for one person, and flags for one session.
Match each setting to the file or flag that holds it.
Who needs the setting, in which projects, and for how long?
Exercise
Take the four layers from this lesson and change one thing: the session
now also starts with --allowedTools "Bash(git push *)". On paper, write
down the outcome for these six tool calls and the layer that decided
each: git push origin main, python3 -m unittest -q,
python3 todo.py add milk, an edit to todo.py, an edit to
todos.json, and git commit -m fix. The result is six lines of the
form the program prints. Doing this once by hand is what lets you
explain a surprising prompt, or a surprising silence, instead of working
around it.
A good result: the push is still denied by the committed file, because a
deny from any layer is read before an allow from any layer, so the new
flag doesn’t change the outcome. The test command and the add command
are both allowed by your local file, whose wide python3 rule is checked
before the committed rule that was written for the test command, because
the allow pass reads the layers highest first. The edit to todo.py is
allowed by the committed file, the edit to todos.json is denied by the
committed file, and the commit prompts, because no rule matches and the
mode is default from the committed file. To check your answer, copy the fixture and add
the flag rule to the flag layer as an allow list. Then add the missing
calls to ACTIONS and run it. Reflection: in a repository you work in every day,
which entries of your own .claude/settings.local.json would the team
want in the committed file, and which would they want gone?
Stretch: Add an ask rule for Edit(todo.py) to the user layer as well, and check whether the committed allow rule for the same path still lets the edit through.
Recap
- Claude Code’s settings come from five levels. Highest first: the organization’s managed settings, the command line for one session, the uncommitted local project file, the committed project file and the user file. A key with one value takes the highest layer that sets it [1].
- The permission rule lists merge across files. Every file adds entries, none removes another’s, and the merged lists are read deny, then ask, then allow. The layer that decided a call is the one that holds the first matching rule in that order, so a deny in any file blocks what an allow in any other file would approve [2].
--permission-modebeatsdefaultModefrom every file for one session,--allowedToolsadds allow rules and--disallowedToolsadds deny rules for the session, and none of them overrides a deny in a settings file [2].- The committed file holds what the whole team needs and gets reviewed with the code. The user file is for your preferences in every project. The local file keeps your exceptions for one project, receives the “don’t ask again” approvals, and stays out of git [1].
- When a setting seems ignored, find the same key in a higher layer.
When a command runs unprompted, open
/permissionsand read which file the matching rule came from [2].
You can now
- Sets permissions to the least the work needs
References
Section titled “References”- Anthropic. Settings files and precedence. Claude Code documentation. Reference.
Claude Code settings - Anthropic. Configure permissions. Claude Code documentation. Reference.
Claude Code permissions