All sections

05Foundations

Settings and env

The one subsystem with real, deterministic precedence. Five layers, strict JSON with no comments, and an env block that every command Claude runs inherits.

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.