Claude Code reads its settings from ~/.claude/settings.json (you, in every project), .claude/settings.json in the project (shared, meant to be committed), .claude/settings.local.json in the project (only you, kept out of git) and managed-settings.json or another managed source your organization deploys. When two of them set the same key, the higher one in the table below wins: managed settings, then --settings on the command line, then project local, then shared project, then user. List keys such as permissions.allow are combined across the files instead of one replacing the other.

WinsLevelFileWho it affects
1Managedmanaged-settings.json”Everyone your organization deploys it to”
2Command line--settings <file-or-json>”for that session only”
3Project local.claude/settings.local.json”You, in this one project only”
4Shared project.claude/settings.json”Everyone working in the folder that contains it”
5User~/.claude/settings.json”You, in every project on this machine”

The file locations, the order and the quotes come from Claude Code’s Settings files and precedence page, read on 2026-10-12. That page puts it in one sentence: “When the same key appears in more than one place, Claude Code uses the value from the highest level that sets it.” The rest of this page is a complete example file, that file checked against the published JSON schema and by claude doctor, and the commands that ran on this machine, where claude --version printed 2.1.296 (Claude Code).

claude --version
2.1.296 (Claude Code)

Where each file lives

A bare .claude means the folder inside your project, and ~/.claude the one in your home directory. In the docs’ words: “In the File column, ~/.claude is the .claude folder in your home directory, and a bare .claude is the .claude folder inside your project.” Two details change the user path. On Windows, “~/.claude means %USERPROFILE%\.claude”, and if CLAUDE_CONFIG_DIR is set, “Claude Code then stores your settings, session history, and plugins there instead.” The commands below ran with CLAUDE_CONFIG_DIR set, so their user file is settings.json inside that directory.

Nothing creates these files at install: “Installing Claude Code doesn’t create any settings file.” Claude Code writes the user file the first time you change a /config option stored there, and the project-local file the first time you answer a permission prompt with “Yes, and don’t ask again”.

The managed file sits in a system directory. The managed settings page names three, one per operating system:

  • macOS: /Library/Application Support/ClaudeCode/managed-settings.json
  • Linux and WSL: /etc/claude-code/managed-settings.json
  • Windows: C:\Program Files\ClaudeCode\managed-settings.json

The same page warns that “Claude Code doesn’t read the legacy Windows path C:\ProgramData\ClaudeCode\managed-settings.json.” An organization can also deliver managed settings through MDM or from the claude.ai admin console, with no file on your disk at all.

There is a fifth file that is not a settings file: “Claude Code also keeps a fifth file, ~/.claude.json, that it writes for itself; you don’t need to edit it.” It holds your sign-in, your MCP servers and per-project state, not the keys on this page.

One subtlety about where the project-local file is read from: “If you start Claude Code in a subdirectory of a git repository, it reads and writes that file at the repository root and applies the approval across the whole repository.” The shared file is different: “Claude Code reads the shared .claude/settings.json from the session’s primary working directory, so to use a file committed at the repository root, start Claude Code there.”

Which one wins, and when nothing wins

The order in the table is a plain override for single values. A model set in .claude/settings.local.json beats the same key in the committed .claude/settings.json, which beats your user file. Arrays work differently: “When you set the same list key, such as permissions.allow, in more than one file, Claude Code combines the lists instead of picking one, so each file can add entries without removing another file’s.” So a deny rule your team commits stays in force even if your local file has its own permissions block.

Environment variables are not a level in the stack. The docs: “An env block inside a settings file is an ordinary key and follows the levels above.” A variable exported in your shell is a separate question, settled per variable. For ANTHROPIC_MODEL, the shell export “applies over the model key from any file”.

Managed settings sit above everything you control: “Nothing in your own settings files or --settings overrides a managed key”. To see which files actually loaded, run /status inside a session; its Setting sources line “lists each settings file Claude Code loaded for the current session”. It names the files, not which file supplied each key.

A complete settings.json example

This full example combines the keys the other Claude Code posts on this site each use one of: permission rules, a default permission mode, environment variables, a hook and a status line.

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": [
      "Bash(npm run lint)",
      "Bash(npm run test *)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)"
    ],
    "defaultMode": "acceptEdits"
  },
  "env": {
    "BASH_DEFAULT_TIMEOUT_MS": "300000",
    "DISABLE_TELEMETRY": "1"
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh"
          }
        ]
      }
    ]
  },
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 2
  }
}

Every key, and where its value comes from in the docs:

  • $schema: “The $schema line points to the published JSON schema for Claude Code settings, which gives you autocomplete and inline validation in VS Code, Cursor, and any other editor that supports JSON schema.”
  • permissions.allow and permissions.deny: the four rules are the docs’ own example from Edit a settings file, which lets lint and test commands run “without asking and stop it reading .env files”. Deny is checked first: “Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity doesn’t change the order.” Which rule forms work, and which file to put them in, is in Stop Claude Code Asking Before Safe Commands.
  • defaultMode: from the permissions page, “To change the mode sessions start in, set defaultMode in your settings files.” The value acceptEdits “Automatically accepts file edits and common filesystem commands such as mkdir, touch, mv, and cp for paths in the working directory or additionalDirectories”. The other documented values include default, plan, auto, dontAsk and bypassPermissions.
  • env: the settings reference says it sets “environment variables for every session and for the subprocesses Claude Code starts from it”. Values are strings: “object mapping variable names to string values”. BASH_DEFAULT_TIMEOUT_MS is, per the environment variables page, the “Default timeout for a foreground Bash or PowerShell tool command, in milliseconds (default: 120000, or 2 minutes)”; 300000 makes it five minutes. DISABLE_TELEMETRY is one of the variables that only check presence: “any non-empty value including 0 turns the behavior on”. Setting it to "0" does not turn telemetry back on; remove the line instead. Not every variable can go in a project file: “Project and local settings can’t set variables that a checked-out repository shouldn’t control; set those in your shell, user settings, or managed settings instead.” The list includes CLAUDE_CONFIG_DIR; the two variables here are not on it.
  • hooks: the PostToolUse entry is the hooks page example that “uses ${CLAUDE_PROJECT_DIR} to run a style checker from the project’s .claude/hooks/ directory after any Write or Edit tool call”. The script itself is yours to write; a hook that formats each edited file is a common one.
  • statusLine: from the status line page, “Add a statusLine field to your user settings (~/.claude/settings.json, where ~ is your home directory) or project settings. Set type to "command" and point command to a script path or an inline shell command.” The padding value is the docs’ own example. A script that prints the model, branch and context use is in Claude Code Status Line.

Which file should this go in? Below it was checked as the project file, .claude/settings.json. In a real repository you would split it: the permissions and the hook go in the committed .claude/settings.json so teammates get them, and the status line goes in your user file because the script lives in your home directory. The docs list “Team permissions, hooks, plugins, and the environment variables the project needs” as what the shared project file is for.

The file is strict JSON: “a // comment or a trailing comma is a syntax error, and Claude Code reports the file as a Settings Error at the next start.”

Checked against the JSON schema

The $schema URL is https://json.schemastore.org/claude-code-settings.json. It was fetched on the same day: 231533 bytes, sha256 0ad8cf34c55f31c4f29d621d43ae4078c462589d9b5c5bcf8d96694a5ae666ba, a draft-07 schema with 142 top-level properties. The schema is not copied into this site. The parts the example touches are recorded in the evidence file, and a test walks every key in the example and checks that the schema has a property of that name with a matching type: an object, array, string or number where the schema says so, defaultMode inside its list of allowed values, type equal to the "command" the schema requires, and each permission rule matching the schema’s rule pattern.

Every key passed. Two things to know about the schema:

  • permissions, hooks and statusLine each reject keys the schema does not list, so a misspelt key inside permissions is flagged by an editor that reads the schema. The top level of the file accepts unknown keys, so a misspelt top-level key is not flagged at all.
  • The docs say “The schema can lag behind the newest CLI releases, so a validation warning on a recently documented key doesn’t mean your configuration is invalid.” The published schema is a helper for your editor; claude doctor, below, is Claude Code’s own check.

claude doctor checks the file without a session

claude doctor is listed in the CLI reference as “Print read-only installation and settings diagnostics from the terminal without starting a session, including install health, settings-file validation errors, and Remote Control eligibility.” The debugging page says: “To find invalid settings files, run claude doctor from your terminal.”

Each command from here on ran in an empty git repository with the example above saved as .claude/settings.json, and with HOME and CLAUDE_CONFIG_DIR pointed at scratch directories and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and DISABLE_TELEMETRY set to 1. No Claude Code session was started and no model was called. Every block ran under strace, which counted zero connect() calls to a network address.

With the example as it is, the Invalid settings section of the report is missing, so grep -c counts zero (and exits 1, which is what grep does when it finds nothing):

claude doctor | grep -c 'Invalid settings'

Exit code 1:

0

The same file with one letter in the wrong case:

sed -i 's/"acceptEdits"/"acceptedits"/' .claude/settings.json
claude doctor | sed -n '/^Invalid settings/,/^$/p'
Invalid settings
- /home/loopdev/dankdev/node_modules/.scratch/ccsettings/repo/.claude/settings.json › permissions.defaultMode: Invalid value. Expected one of: "acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"
  Suggested fix: Valid modes: "acceptEdits" (ask before file changes), "plan" (analysis only), "bypassPermissions" (auto-accept all), or "default" (standard behavior)

The report names the file, the dotted path of the key and the values it accepts. Values are case-sensitive. An editor that reads the $schema line flags the same mistake before you save, because the schema’s list for defaultMode has no acceptedits either. The list claude doctor printed leaves out manual, which the permissions page says Claude Code accepts as an alias of default (“Labeled Manual in the CLI, the VS Code and JetBrains extensions, and the desktop app, and Claude Code accepts manual as an alias”). Changed to "manual", the file passed: zero Invalid settings sections. (The last line puts acceptEdits back; the count comes from the grep line, and the block’s exit code from that last sed.)

sed -i 's/"acceptedits"/"manual"/' .claude/settings.json
claude doctor | grep -c 'Invalid settings'
sed -i 's/"manual"/"acceptEdits"/' .claude/settings.json
0

The one subcommand that writes a settings file

On 2.1.296, claude --help lists no config command. The documented way to change a setting from inside Claude Code is the /config menu, which needs a session, so it was not run here. The docs list where it saves: most options to ~/.claude/settings.json, a few such as Show tips to .claude/settings.local.json, and the global config options to ~/.claude.json.

One documented subcommand does write a settings file with no session. The CLI reference describes claude auto-mode reset as: “Restore the default auto mode configuration by removing the autoMode section from your user settings file. Prompts for confirmation before writing; pass -y/--yes to skip the prompt.” Here it is with an autoMode block from the settings reference example, next to a statusLine, in the user file:

cat > "$CLAUDE_CONFIG_DIR/settings.json" <<'EOF'
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  },
  "autoMode": {
    "soft_deny": ["$defaults", "Never run terraform apply"]
  }
}
EOF
claude auto-mode reset --yes
cat "$CLAUDE_CONFIG_DIR/settings.json"
This resets auto mode to the shipped defaults by removing the autoMode section from /home/loopdev/dankdev/node_modules/.scratch/ccsettings/cfg/settings.json:
  - soft_deny (2 entries)
Auto mode configuration reset to defaults — autoMode section removed from /home/loopdev/dankdev/node_modules/.scratch/ccsettings/cfg/settings.json.
Run `claude auto-mode config` to see the effective rules.
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  }
}

It wrote the user settings file and nothing else: the statusLine stayed, and .claude/settings.json in the repository was byte for byte the same before and after. Because CLAUDE_CONFIG_DIR was set, the user file was settings.json in that directory. Without it, the same command edits ~/.claude/settings.json.

Keep settings.local.json out of git

When Claude Code creates .claude/settings.local.json itself, it handles git for you: “The first time Claude Code writes the file in a git repository that doesn’t already ignore it, it adds **/.claude/settings.local.json to your global git excludes file, so the file stays out of your commits in every repository.” It also says: “If you created the file by hand and Claude Code hasn’t written to it yet, add it to .gitignore yourself.” The line to add is the same pattern:

printf '%s\n' '**/.claude/settings.local.json' >> .gitignore
echo '{}' > .claude/settings.local.json
git check-ignore -v .claude/settings.local.json
.gitignore:1:**/.claude/settings.local.json	.claude/settings.local.json

git check-ignore -v printed the .gitignore line that matched, so the file will not show up in git status. Commit .claude/settings.json; never commit the local file.

What was run here, and what was not

The version came from claude --version, which the CLI reference lists as “Output the version number”. Every command block above that has output under it ran on 2.1.296 in that order, in one scratch git repository, with the output printed as it came out. The output is stored alongside the settings files each command left behind, and a test in this site’s repository runs the blocks again in a fresh scratch directory and fails if any output, exit code or file differs. The md5 of this machine’s real .claude.json was the same before and after.

Not run: any Claude Code session, the /config menu, /status, and claude --settings, all of which start a session. What a session does with each file, and the precedence order itself, is quoted from the docs above, not shown.

← Back to all posts

Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.

Back to the top