DEV Community

Rulestack
Rulestack

Posted on Edited on

Claude Code settings precedence: which of your five settings files actually wins

You set permissions.defaultMode in one settings file, restart Claude Code, and a different mode is active. Nothing is broken — another file won.

Claude Code reads settings from up to five places, and when the same key appears in more than one, a fixed precedence decides which value applies. Most "my setting doesn't work" reports are really precedence questions. Here's the whole model, verified against the official settings docs as of Claude Code 2.1.22x.

The five layers

From highest priority to lowest:

  1. Managed settings — deployed by an organization (server-delivered, MDM/OS policy, or a system-level managed-settings.json). Nothing below can override these, with a handful of deliberate security exceptions I'll get to.
  2. Command line — JSON passed via --settings <file-or-json>. Temporary, session-scoped.
  3. Local project settings.claude/settings.local.json at the repository root. Yours, this repo only.
  4. Shared project settings.claude/settings.json, checked into the repo. The team's.
  5. User settings~/.claude/settings.json. Yours, every project.

If your user file sets spinnerTipsEnabled: true and the project file sets it to false, the project value applies. That's the whole rule for scalar values: higher layer wins, key by key. A key you don't set in a higher layer falls through to the lower one — layers merge, they don't replace each other wholesale.

The part that surprises people: arrays merge, they don't override

Scalar values override. Array values concatenate and deduplicate across scopes.

That includes the arrays people care most about: permissions.allow, permissions.deny, sandbox.filesystem.allowWrite, and friends. If the shared project file allows Bash(npm test:*) and your local file allows Bash(pnpm test:*), both rules are active. Lower-priority scopes can add entries; they cannot remove entries a higher scope contributed — and vice versa.

Two practical consequences:

  • You can't "turn off" a team permission rule from your local file. There is no negative entry. If a checked-in allow rule is too broad for your taste, that's a conversation with your team, not a local override.
  • Permission problems are often additive surprises: a rule you forgot in your user file is still merging into every project.

Two arrays are exceptions and do not merge this way: fallbackModel is an ordered chain, so the highest-precedence file that defines it supplies the entire list; and a managed availableModels allowlist applies as-is — user, project, and local entries can't extend it.

Where each rule belongs

A decision rule that has held up well:

You're setting... Put it in
Personal defaults you want everywhere (theme, editor, your own allow rules) ~/.claude/settings.json
Anything the team should share (project permissions, hooks, env) .claude/settings.json (checked in)
Personal experiments, machine-specific paths, permissions you're trialing .claude/settings.local.json
One-off session behavior --settings on the CLI

The test for "shared vs local" is: would a teammate's session be wrong without this? If yes, it's .claude/settings.json. If it only makes your machine work, it's local.

Two gotchas in the local file

The auto-gitignore only happens when Claude Code writes the file. When Claude Code itself saves a setting into .claude/settings.local.json in a repo that doesn't already ignore it, it adds **/.claude/settings.local.json to your global git excludes. If you create the file by hand, that step never runs — add it to your gitignore yourself, or you'll commit your personal settings to the team repo.

Its location changed in v2.1.211. The file now belongs at the repository root. Older versions wrote it into whatever directory you started Claude Code from, and current versions still read a file an older version left there. When both exist and set the same key, the repo root's value wins — except permission rules, which follow the array rule above: rules from both files stay in effect.

The security exceptions

A few security-sensitive keys deliberately break the "higher layer wins" rule, in the restrictive direction only:

  • disableClaudeAiConnectors: true applies from any scope, even when a managed source says false.
  • crossSessionInbound (how sessions accept messages from other sessions) uses an accept < hold < refuse ladder, and a stricter value in project or local settings beats managed and user values. A checked-in accept can never loosen your personal refuse.

The design intent is consistent: a random file in a cloned repo can tighten your security posture, but it can never loosen it.

How to see what actually loaded

Don't reason about precedence from memory — inspect it:

  • /status → the Status tab has a Setting sources line listing every layer loaded this session (User settings, Project local settings, Enterprise managed settings (remote), ...). A layer only appears if it loaded with at least one key. Note it shows which sources loaded, not which layer supplied each individual key.
  • claude doctor → details when a settings file fails to parse or validate. An interactive session also shows a Settings Error dialog at startup and lets you continue without the broken file — which is its own trap: your session then runs without settings you believe are active.

The mental model in four lines

  1. Five layers; higher wins per key; managed is (almost) absolute.
  2. Scalars override. Arrays merge — you can add, you can't subtract.
  3. Local is for you, project is for the team, user is for every repo.
  4. When in doubt, /status beats guessing.

I maintain Rulestack — ready-to-fork rule packs, skills, and hooks for Claude Code, Cursor, and Codex. The precedence notes above come from keeping those packs verified against each release.

Daily notes on AI coding agents on Bluesky: @ai-shop.bsky.social

Top comments (2)

Collapse
 
deanlee profile image
Dean Lee

The array-merge rule for permissions is the detail most people miss and the one that causes the most subtle bugs. You frame it as "you cannot remove a higher-scope entry," which is correct but undersells the debugging pain. When a permission rule you did not write is silently merged into every session, the failure mode is not a clear error. It is a behavior that looks right most of the time, until the merged rule and the task collide in a way that takes twenty minutes to trace back to a team settings.json you never opened.

One thing I would add from the /context discussion we have been having: the settings precedence model and the /context load model should be paired more explicitly. A user can inspect which settings file contributed which value with claude config show, but /context does the same mapping for system prompts and rules. The mental model is the same—layers merge, a higher layer can shadow but not delete—but the tooling surfaces them differently, which creates an illusion that they are separate problems.

The settings.local.json gitignore trap is worth its own section. I have seen that exact bug twice this month.

Collapse
 
rulestack profile image
Rulestack

The failure-mode framing is better than mine. "Not a clear error" is exactly why it eats twenty minutes — a denial at least tells you where to look, while a rule that quietly widens what's allowed produces a session that behaves plausibly and only diverges on one specific combination.

On pairing the two models: I want to be careful about promising that a particular command does the settings-provenance mapping, because I haven't confirmed that one myself, and the last time I wrote a config detail from memory I had it backwards. But the underlying point holds regardless of which tool surfaces it — both are layered merges where a higher layer shadows without deleting, and showing them through different surfaces is what makes people file them as two separate problems.

The settings.local.json gitignore trap probably does deserve its own post. Twice in a month is more than I've run into — was it the same shape both times (the local file exists for one person, so the behaviour differs across the team and nobody's config looks wrong), or two different failures?