Skip to content

AI CODING ASSISTANTS

Claude Code settings.json: Which File Actually Wins

Claude code settings.json exists in five scopes that merge rather than replace. We measured which one wins, and the two ways a settings file silently does nothing.

Claude code settings.json is not one file, it is five scopes that get merged into one effective configuration, and the question that actually costs people time is which one wins when two disagree. We measured that on Claude Code 2.1.259 on 2026-09-03 by putting different models in different scopes and reading back which model the session used. The ladder is unambiguous, the scopes merge rather than replace, and two things will make a settings file do nothing at all without telling you. The Claude Code guide covers the session these files configure.

Key takeaways

  • Precedence, strongest first: --model flag → --settings <file>.claude/settings.local.json.claude/settings.json~/.claude/settings.json, with admin policy above all of them.
  • Scopes merge key by key. A model in the project file and a deny rule in the local file both applied in the same session.
  • A deny rule in the weaker scope beat an allow in the stronger one — precedence resolves scopes, not the deny-over-allow rule.
  • Project settings do not walk up the directory tree. A session started in a subdirectory ignored the settings file one level above it.
  • Malformed JSON is dropped silently in print mode: no warning, session starts anyway. claude doctor is the only place it surfaces.

The short answer

Put things you want everywhere in ~/.claude/settings.json. Put things the team should share in the repository's .claude/settings.json and commit it. Put machine-specific overrides in .claude/settings.local.json, which is gitignored and which beats the committed file. If two of them set the same key, the more specific one wins; if they set different keys, both apply.

Everything below is that claim, measured. The probe is one non-interactive turn per configuration, reading the resolved model out of --output-format json — the model key is the only setting whose resolution comes back in machine-readable form, which makes it the tracer for all the others.

Terminal
claude -p "Reply with the single word: ok" --output-format json
# → "modelUsage": { "claude-sonnet-5": { ... } }
#   the winning scope named sonnet; every other scope named something else

Where claude code settings.json lives

Five places, and only three of them are files you write by hand. Anthropic's settings reference is the authority on which keys each file accepts; what follows is the authority on which file wins.

ScopePathCommitted
Managed policyadmin-installed, machine-widen/a
Flag--settings <file-or-json>n/a
Local.claude/settings.local.jsonno — gitignored
Project.claude/settings.jsonyes
User~/.claude/settings.jsonno

On the machine this article was written on, the user file is 171 bytes and holds four keys — autoUpdatesChannel, theme, agentPushNotifEnabled and enabledPlugins. That is a realistic user file: it is where preferences go, not where policy goes. The repository's .claude/settings.local.json is 4,263 bytes and holds two keys, permissions and enabledMcpjsonServers, with 21 allow rules and no deny rules — which is what a settings file looks like after a few weeks of answering permission prompts.

The managed tier could not be measured here. claude doctor reports it plainly:

Terminal
claude doctor
# → Managed settings (remote): not fetched — requires an Enterprise or Team subscription

So the position of admin policy at the top of the ladder is Anthropic's documented behaviour, not our measurement, and this article marks it as such. Everything below that line was measured.

The five scopes, in the order they win

Each row below was a session in which the named scope and every scope beneath it disagreed about the model. The model that came back names the winner.

RankScopeSet toSession used
1--model flaghaikuhaiku
2--settings extra.jsonhaikuhaiku
3.claude/settings.local.jsonsonnetsonnet
4.claude/settings.jsonhaikuhaiku
5~/.claude/settings.jsonno model keyaccount default

The two rows that matter in daily use are 3 and 4. settings.local.json beats settings.json, which is the right way round — the file you did not commit is the one that reflects this machine — but it also means a teammate debugging why your session behaves differently from the committed config is looking at the wrong file.

--setting-sources is the flag that proves the layering rather than the ordering, because it removes scopes rather than outranking them:

FlagFiles readSession used
noneproject + localsonnet (local wins)
--setting-sources projectproject onlyhaiku (local dropped)
--setting-sources useruser onlyaccount default (both dropped)

That last row is the useful one for debugging. If a session behaves oddly and you want to know whether the repository is responsible, --setting-sources user answers it in one run without moving any files. The bigger hammer is --safe-mode, which drops every customization at once and is measured in full here.

Claude Code settings precedence ladder measured across five scopes with the resolved model each time
Five scopes, five sessions, one tracer setting. Claude Code 2.1.259, 2026-09-03.

The scopes merge, they do not replace

This is the question the precedence table does not answer, and it is the one that decides how you organise your files. If the winning scope replaced the losing one, a single key in settings.local.json would silently disable everything in the committed settings.json.

It does not. We put the model in the project file and a deny rule in the local file, then asked for a shell command:

.claude/settings.json
{
  "model": "haiku"
}
.claude/settings.local.json
{
  "permissions": {
    "deny": ["Bash"]
  }
}
Terminal
claude -p "Run the shell command `echo merged` and tell me what it printed." --output-format json
# → model:  claude-haiku-4-5-20251001      ← from settings.json
# → result: "I don't have shell execution tools available in this session."

Both applied. The model came from the project file and the deny rule came from the local file, in one session. The control run — same project file, no local file — printed merged, so the deny rule is what removed the tool rather than the model declining.

Two details in that result are worth more than the merge finding itself. First, permission_denials in the JSON output stayed empty: a denied tool is subtracted from the tool list rather than intercepted when it is called, so the model does not experience a refusal, it experiences an absence. Second, when we inverted the files — deny: ["Bash"] in the committed project file, allow: ["Bash"] in the higher-ranked local file — the deny still held. Scope precedence decides which value of a key wins; it does not promote an allow over a deny. That evaluation order is covered in full in Claude Code permissions.

Project settings do not walk up the tree

We put a settings file in a directory, then started a session in a child folder that had no .claude of its own.

The layout under test
tmp/
  .claude/
    settings.json      ← { "model": "sonnet" }
  child/               ← session started here, no .claude

The session came back on the account default, not sonnet. Repeated, same result. Project settings are read from the session's working directory; they are not inherited downward from a repository root.

That is a specific, expensive failure in two common layouts. In a monorepo where .claude/settings.json sits at the root and you work in packages/api, none of it applies. And on this machine there are literally two settings files one directory apart — 65 allow rules in the parent of the repository, 21 inside it — and which one governs a session is decided entirely by where the session was started. Neither file knows the other exists.

The fix is --add-dir for the paths you need access to, or a .claude directory in each working root you actually start sessions from. There is no upward search to configure.

The two ways a settings file does nothing

An unknown key is ignored, and the rest of the file still applies. We wrote a file with one good key and two bad ones:

.claude/settings.json
{
  "model": "sonnet",
  "modell": "haiku",
  "nonsenseKey": 42
}

The session used sonnet. Unrecognised keys do not invalidate their neighbours, which is forgiving of a typo in a key you did not need — and unforgiving of a typo in the key you did, because modell produced no message at all. If a setting appears to have no effect, the first thing to check is the spelling of the key, and the second is whether the loader reads it at all.

Malformed JSON drops the entire file, silently. A trailing comma is enough:

.claude/settings.json — a trailing comma
{
  "model": "sonnet",
}
Terminal
claude -p "Reply with the single word: ok" --output-format json
# → account default, not sonnet
# → exit 0, nothing on stdout or stderr about the settings file

The session started normally and the file did nothing. This is documented behaviour rather than a defect — the CLI reference and claude --help both say settings files that fail validation are silently ignored in print mode, because there is no dialog to show — but it is a very quiet failure to have on the other end of a CI pipeline. claude doctor is where it surfaces:

Terminal
claude doctor
# → Invalid settings
# → - <path>/.claude/settings.json: Invalid or malformed JSON

If you take one operational habit from this article, make it that: run claude doctor after editing a settings file. It is the only thing on this list that reads the file and tells you what it thinks.

What did not work

Two results went sideways, and both are worth recording.

The first probe design was wrong and produced a confidently false answer. Reading the resolved model as the first key of modelUsage returned claude-haiku-4-5 for every configuration, including one with no settings files at all — which would have supported a tidy, completely incorrect conclusion that none of the scopes did anything. The cause is that a session can report more than one model: a background classifier turned up alongside the main one with 899 tokens against the main model's 25,000. Sorting by token count instead of taking the first key produced the ladder above, and every row of it was then reproduced in a second round. If you are reading modelUsage in your own scripts, sort it.

Prompt size turned out to depend on the model, which nothing in this experiment was meant to test. The same empty directory and the same one-line prompt produced:

Resolved modelPrompt tokens
haiku25,397
opus26,733
sonnet37,863

Sonnet's session prompt was 12,466 tokens larger than haiku's for identical input, reproduced twice at each model. We did not investigate the mechanism and are not going to guess at one. The operational note is narrow: a model line in a settings file changes what every request costs by more than the per-token price difference implies, and if you are budgeting context, context window management is where that gets handled properly.

Best practices

  • Commit .claude/settings.json, gitignore .claude/settings.local.json. The first is the team's contract, the second is your machine. The precedence order was designed for exactly that split.
  • Put deny rules in the committed file. They cannot be overridden from a local file, which is what makes them worth committing.
  • Run claude doctor after every edit. It is the only surface that reports a malformed file, and print-mode sessions will not.
  • Use --setting-sources user to bisect. One run tells you whether the repository's configuration is involved, without moving or renaming anything.
  • Keep a .claude directory in each root you start sessions from. Nothing searches upward, so a monorepo root config does not reach a package directory.
  • Use --settings for one-off experiments. It outranks both project files and leaves nothing behind on disk.

Common mistakes

  • Expecting a monorepo root config to apply everywhere. It applies to sessions started at the root. Started anywhere below, it is not read.
  • Editing settings.json while settings.local.json sets the same key. The local file wins and there is no indication that your edit was outranked.
  • Assuming a local allow re-enables a denied tool. It does not. Deny survives every scope above it.
  • Trusting a silent start. A session that starts cleanly is not evidence that your settings file parsed. A trailing comma produces exactly that outcome.
  • Adding keys the loader does not read. They are ignored without comment, so a plausible-looking key can sit in your config for months doing nothing — the same failure mode as writing frontmatter fields nothing consumes.

Conclusion

Write three files and know their order: ~/.claude/settings.json for preferences, a committed .claude/settings.json for what the team shares including every deny rule, and .claude/settings.local.json for this machine, which outranks the committed file. Remember that they merge rather than replace, so a key set once always applies, and that nothing searches upward — the directory you start the session in decides which project config exists at all. Then run claude doctor, because it is the only thing that will tell you the file you just edited is invalid. Next, read Claude Code permissions for the rule evaluation that sits on top of these scopes.

Frequently asked questions

Where is the Claude Code settings.json file?
There are three you write by hand: ~/.claude/settings.json for your account, .claude/settings.json in a repository for settings you commit, and .claude/settings.local.json beside it for settings you do not. A managed policy file and the --settings flag add two more scopes. All five are read and merged into one effective configuration.
Which Claude Code settings file takes priority?
Measured on 2.1.259, strongest first: the --model flag, then --settings <file>, then .claude/settings.local.json, then .claude/settings.json, then ~/.claude/settings.json. Admin-managed policy settings sit above all of them. Each row in that ladder beat the row below it in a session where the two named different models.
Do Claude Code settings files override or merge?
They merge, key by key. In a session where .claude/settings.json set the model and .claude/settings.local.json set a deny rule, both took effect: the model came from the project file and Bash was gone from the tool list. Only a key set in two files is resolved by precedence; keys set in one file always apply.
Does .claude/settings.json apply to subdirectories?
It applies to sessions started in that directory, not to sessions started below it. We put a settings file in a directory, started a session in a child folder with no .claude of its own, and the settings did not apply — the account default came through instead. In a monorepo, the working directory decides which project settings you get.
Why is my settings.json being ignored?
The most likely cause is invalid JSON. A trailing comma made the whole file drop out of a print-mode session with no message on stdout or stderr; the session started normally, just without the settings. Run claude doctor, which does report it as Invalid settings, or check that you edited the file the working directory actually reads.

Muhammad Kashif

Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.