# Where a setting comes from

> Why a coding agent's setting seems ignored: the settings files in override order, one worked example where four layers disagree and the effective permission set is derived entry by entry, and what a team commits versus what each person keeps local.

Page: https://schubergphilis.github.io/ai-training/customizing-agents/settings-layering/
License: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/

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 (Settings files and precedence, Claude Code documentation). 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

**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 (Settings files and precedence, Claude Code documentation).

| 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 (Settings files and precedence, Claude Code documentation).

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 (Settings files and precedence, Claude Code documentation). 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 (Configure permissions, Claude Code documentation). 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 (Configure permissions, Claude Code documentation). 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 (Configure permissions, Claude Code documentation).

#### Checkpoint: Order the layers

- Flags on the command that starts the session
- Settings the organization deploys
- The committed file in the project
- The file in your home directory
- The uncommitted local file in the project

## 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 *)"`.

```json
// ~/.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.

```python
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.

#### Checkpoint: Derive the effective set

What does this print, one line per tool call?

```python
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)}")
```

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

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 (Settings files and precedence, Claude Code documentation). 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 (Configure permissions, Claude Code documentation). 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 (Settings files and precedence, Claude Code documentation).

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 (Settings files and precedence, Claude Code documentation).

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 (Settings files and precedence, Claude Code documentation). Managed settings, when your organization
uses them, sit above everything, and no file of yours and no flag
overrides a managed permission rule (Configure permissions, Claude Code documentation).

#### Checkpoint: Which layer does it belong in?

Buckets:

- User file
- Committed project file
- Project local file
- Session flag

Items:

- Deny reads of the .env file for everyone on the team
- Your dark theme
- Allow the test command in this repository for everyone
- A wider allow rule you are trying out before you propose it
- Start this one run in plan mode
- Your own read-only git rules for every project

#### Pitfall: The committed allowlist that grew in the dark

A team reviews its committed `.claude/settings.json` and agrees that the
only `python3` command that runs without a prompt is
`Bash(python3 -m unittest *)`. One developer, tired of prompts while
trying things, adds `Bash(python3 *)` to their `.claude/settings.local.json`.
The lists merge, so in that developer's sessions the effective allow set
holds both rules, and `python3 todo.py add`, `python3 deploy.py` and
every other `python3` command run unprompted. The committed file still
shows one narrow rule, the local file is out of git, and a reviewer
reading the repository doesn't see a change. The wide entry came from the local
layer. Only the `/permissions` dialog says so, because it lists each rule
from your settings files with the file it came from
(Configure permissions, Claude Code documentation). So when a command runs without the prompt
you expected, open `/permissions` and read the file column before you
touch the committed file, and prune your local file the way the last
lesson pruned the allowlist.

## 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

1. 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 (Settings files and precedence, Claude Code documentation).
2. 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
   (Configure permissions, Claude Code documentation).
3. `--permission-mode` beats `defaultMode` from every file for one
   session, `--allowedTools` adds allow rules and `--disallowedTools`
   adds deny rules for the session, and none of them overrides a deny in
   a settings file (Configure permissions, Claude Code documentation).
4. 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
   (Settings files and precedence, Claude Code documentation).
5. When a setting seems ignored, find the same key in a higher layer.
   When a command runs unprompted, open `/permissions` and read which
   file the matching rule came from (Configure permissions, Claude Code documentation).

## References

- Settings files and precedence, Claude Code documentation, https://code.claude.com/docs/en/settings
- Configure permissions, Claude Code documentation, https://code.claude.com/docs/en/permissions
