The hierarchy
Unlike memory and rules, settings have a real, deterministic precedence order. Higher entries win outright.
graph TD
A["1. Enterprise managed policy<br/><i>/etc/claude-code/managed-settings.json</i>"] --> B["2. Command-line arguments<br/><i>--model, --permission-mode, ...</i>"]
B --> C["3. .claude/settings.local.json<br/><i>yours, this project, gitignored</i>"]
C --> D["4. .claude/settings.json<br/><i>team, committed</i>"]
D --> E["5. ~/.claude/settings.json<br/><i>yours, all projects</i>"]
Read it as: 1 beats 2 beats 3 beats 4 beats 5. If model is set in both
~/.claude/settings.json and .claude/settings.json, the project file wins.
Resolution is per key, not per file. A user-level file still supplies every key the higher-priority files leave unset, so you do not have to restate your whole configuration to override one value.
Strict JSON
Settings files are parsed as strict JSON. No // comments, no trailing
commas, no single quotes. A stray comment produces a Settings Error at startup
and the whole file is ignored, which looks exactly like “Claude Code is ignoring
my configuration.”
Add the schema line for editor validation:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json"
}
What env does
The env block writes key/value pairs into the Claude Code process
environment. It is applied at startup and re-applied whenever the settings file
changes. Because Claude Code reads it from the file directly, it takes effect
regardless of how claude was launched: from a shell, from an IDE, from a
launcher that never sourced your .bashrc.
Everything Claude Code spawns inherits these variables:
graph TD
S["settings.json env block"] --> CC["Claude Code process"]
CC --> B["Bash tool commands<br/><i>make build, go test, mvn verify</i>"]
CC --> H["Hook commands"]
CC --> SL["Status line command"]
CC --> MCP["stdio MCP servers"]
That inheritance is the point. An IDE-launched session that never read your shell
profile still gets the right JAVA_HOME, so mvn verify behaves identically
whether Claude ran it or you did.
Tuning Claude Code itself
Variables it reads for its own behaviour:
| Variable | Effect |
|---|---|
API_TIMEOUT_MS |
Request timeout. Raise on slow networks or behind a corporate proxy |
BASH_DEFAULT_TIMEOUT_MS |
Default Bash tool timeout. The default is often too short for a full build |
BASH_MAX_OUTPUT_LENGTH |
Truncation threshold for command output |
MAX_THINKING_TOKENS |
Thinking budget |
DISABLE_TELEMETRY |
Opt out of usage telemetry |
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH |
How deep subagents may nest |
Setting up the toolchain
The one that matters in a polyglot repo:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"env": {
"JAVA_HOME": "/usr/lib/jvm/java-21-openjdk",
"MAVEN_OPTS": "-Xmx2g",
"GOFLAGS": "-mod=mod",
"GOPRIVATE": "github.example.internal/*",
"PYTHONPATH": "./src",
"NODE_OPTIONS": "--max-old-space-size=4096",
"BASH_DEFAULT_TIMEOUT_MS": "300000",
"API_TIMEOUT_MS": "1200000"
}
}
Since JSON cannot carry comments, here is the annotation separately:
| Key | Why it is there |
|---|---|
JAVA_HOME, MAVEN_OPTS |
Maven and Gradle builds Claude runs use the right JDK and heap |
GOFLAGS, GOPRIVATE |
Module resolution against the internal proxy |
PYTHONPATH |
Imports resolve without an editable install |
NODE_OPTIONS |
Large TS builds stop OOM-ing |
BASH_DEFAULT_TIMEOUT_MS |
300000 = 5 minutes, enough for a full build |
API_TIMEOUT_MS |
Tolerates a slow corporate proxy |
A useful convention: keep the annotation table in CLAUDE.md rather than in a
sibling file nobody opens. It is short, it is genuinely non-obvious, and it stops
the next person deleting a variable they do not recognise.
Behavioural rules for env
| Rule | Detail |
|---|---|
| Settings beat the shell | If a variable is set both in your shell and in env, the settings value wins; it is written into the process environment at startup, replacing the inherited value |
| You can set but not unset | To neutralise a variable coming from a shell profile you don’t control, set it to "" |
| Trust gating | env from a committed .claude/settings.json is withheld until the user accepts the workspace trust prompt. A teammate cloning the repo won’t get it on first run |
| Never commit secrets | A committed settings.json is in git history forever. Tokens go in .claude/settings.local.json or the shell |
Permissions
Permissions live in settings but resolve differently from every other key: they
merge across scopes, and a deny beats an allow from anywhere.
{
"permissions": {
"allow": [
"Bash(make *)",
"Bash(go test *)",
"Bash(git status *)",
"Bash(git diff *)"
],
"ask": [
"Bash(gh pr create *)"
],
"deny": [
"Write(**/generated/**)",
"Edit(**/generated/**)",
"Read(./.env)",
"Read(**/secrets/**)",
"Bash(git push --force *)",
"Bash(rm -rf *)"
]
}
}
| List | Meaning |
|---|---|
allow |
Runs without prompting. Use for the read-only and build commands you approve dozens of times a day |
ask |
Always prompts, even in a permissive mode. Use for outward-facing actions |
deny |
Blocked outright, no prompt, no override. An enterprise deny cannot be overridden at all |
deny on Read is worth knowing about: it is the mechanism that keeps .env
files and credential directories out of the context window entirely, rather than
relying on an instruction asking Claude not to look.
Frequently used keys
| Key | Purpose |
|---|---|
model |
Pin the model for this scope |
outputStyle |
Which output style is active |
agent |
Run the whole session as a named agent |
permissions.allow / .ask / .deny |
Tool permission rules |
claudeMdExcludes |
Glob list of memory files to skip |
hooks |
Inline hook definitions |
enabledPlugins |
Which plugins are active for this project |
statusLine |
Custom status line command |
Splitting committed from local
The division that works in practice:
| File | Holds | In git |
|---|---|---|
.claude/settings.json |
Toolchain env, permissions.deny, enabledPlugins, hooks the team relies on |
Yes |
.claude/settings.local.json |
Tokens, machine-specific paths, your outputStyle, your model |
No, gitignore it on day one |
Add .claude/settings.local.json and CLAUDE.local.md to .gitignore in the
same commit that creates .claude/. Doing it later means doing it after someone
has already pushed a token.