BubbleCodeFounding access

claude settings.json: an annotated reference

Updated July 27, 2026 · 9 min read

Claude Code reads several settings files and merges them, with admin-managed policy at the top and your user file at the bottom. Almost everything worth configuring lives under four keys: permissions, hooks, statusLine, and model.

Verified against Claude Code 2.1.220

Key names and descriptions below come from the 2.1.220 binary's own settings schema. Settings keys are added frequently — treat anything you don't see in your build's /config or claude doctor output as unavailable.

Where the files live

ScopePathUse it for
User~/.claude/settings.jsonYour defaults across every project — statusline, personal hooks, model preference.
Project.claude/settings.json in the repoShared, committed config: permission rules and hooks everyone on the team should get.
Local.claude/settings.local.json in the repoYour personal overrides for that project. Gitignore it.
Managed / policyAdmin-controlled locationEnterprise lockdown. Wins over everything below it, and several keys are honored *only* from here.

They merge rather than replace, with managed settings at the top and user settings at the bottom. --setting-sources user,project,local controls which layers a session loads at all, and --settings <file-or-json> adds an extra one — useful when scripting an agent that shouldn't inherit your personal config.

Can't find where a setting came from?

claude doctor reads settings files in the current directory without a trust prompt, and claude --safe-mode starts with all customizations disabled — hooks, skills, plugins, MCP servers, custom commands. If a problem disappears in safe mode, it's your config.

permissions

The most consequential key. It decides what runs without asking you.

json
{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": ["Bash(npm *)", "Edit(.claude)", "Read"],
    "ask": ["Edit(//etc/*)"],
    "deny": ["Bash(rm -rf *)"],
    "additionalDirectories": ["../shared-types"],
    "disableBypassPermissionsMode": "disable"
  }
}
  • defaultMode — one of acceptEdits, auto, bypassPermissions, manual, dontAsk, plan. What each does.
  • allow / ask / deny — arrays of Tool(specifier) rules. Exact (Bash(npm run test)), wildcard (Bash(git *)), path glob (Edit(docs/**)), or a bare tool name. deny takes precedence.
  • additionalDirectories — extra directories inside the permission scope, the persistent form of --add-dir.
  • disableBypassPermissionsMode — set to "disable" to remove the ability to opt into no-checks mode from this scope.

The colon form (Bash(npm run:*)) still works but the CLI labels it *legacy* — prefer the space wildcard in new config.

hooks

Shell commands run at lifecycle points, described by the schema as *custom commands to run before/after tool executions*. Thirteen events are available; each maps to matcher groups.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/format.sh" }]
      }
    ],
    "PermissionRequest": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify.sh" }]
      }
    ]
  }
}

The matcher uses permission-rule syntax, so Bash(git *) scopes a hook to git commands only and avoids spawning a process on every unrelated tool call. Full event list, payloads, and examples in the hooks guide.

Related keys: disableAllHooks turns off all hooks *and* statusLine execution, and in managed settings there's a lock that makes only managed hooks run, ignoring user, project, and local ones.

statusLine

json
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  }
}

Your command receives a large JSON object on stdin — model, workspace, git repo, context window usage, subscription rate limits, worktree, open PR — and whatever it prints becomes the line. There's a companion key to *re-run the status line command every N seconds in addition to event-driven updates*, which matters if you're rendering something time-sensitive. Full schema and a working script.

model and fallbacks

json
{
  "model": "opus",
  "fallbackModel": ["sonnet", "default"]
}

model overrides the default model. fallbackModel takes a list tried in order when the primary is overloaded or unavailable — each element accepts a name or alias, and "default" expands to the default model. A CLI --fallback-model takes precedence over the setting.

Enterprise deployments also get availableModels, an allowlist accepting family aliases (opus allows any opus version), version prefixes, or full model IDs — typically set in managed settings.

worktree configuration

Underused, and directly relevant if you run several agents in parallel. Fresh worktrees start with no node_modules, and this is the fix:

json
{
  "worktree": {
    "symlinkDirs": ["node_modules", ".cache"],
    "branchFrom": "fresh"
  }
}
  • symlinkDirs — *directories to symlink from main repository to worktrees to avoid disk bloat.* Nothing is symlinked by default; the schema suggests node_modules, .cache, .bin.
  • branchFromfresh (default) branches from origin/<default-branch> for a clean tree; head branches from your current local HEAD so unpushed work is present.
  • There's also a sparse-checkout option for including only some directories, and an isolation mode controlling whether background sessions may edit the main checkout directly or must enter a worktree first.

env, attribution, and housekeeping

json
{
  "env": { "NODE_ENV": "development" },
  "cleanupPeriodDays": 30,
  "attribution": {
    "commit": "",
    "pr": ""
  },
  "autoUpdates": true
}
  • env — environment variables set for Claude Code sessions.
  • cleanupPeriodDays — days to retain chat transcripts before automatic cleanup (default 30, minimum 1). --no-session-persistence disables transcript writes entirely.
  • attribution — attribution text for commits and PRs; an empty string hides it. This replaces the deprecated includeCoAuthoredBy.
  • apiKeyHelper — path to a script that outputs authentication values, for setups that don't use the standard login.

A sensible starting point

Two files. Project settings you commit:

json
// .claude/settings.json — committed, applies to everyone
{
  "permissions": {
    "defaultMode": "acceptEdits",
    "deny": [
      "Bash(rm -rf *)",
      "Bash(git push --force *)",
      "Read(./.env)",
      "Read(./.env.*)"
    ],
    "allow": [
      "Bash(npm run test *)",
      "Bash(npm run lint *)",
      "Bash(git status)",
      "Bash(git diff *)"
    ]
  },
  "worktree": {
    "symlinkDirs": ["node_modules"]
  }
}

And user settings you don't:

json
// ~/.claude/settings.json — yours, everywhere
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  },
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify.sh" }]
      }
    ]
  },
  "cleanupPeriodDays": 90
}

The split matters: permission rules describe what's safe *in this repo* and belong with the repo. Statuslines and notification hooks describe how *you* like to work and belong in your home directory, so they follow you into every project.

FAQ

Where is the Claude Code settings.json file?

There are several. ~/.claude/settings.json holds your personal defaults across all projects, .claude/settings.json inside a repository holds committed project settings, and .claude/settings.local.json holds your personal per-project overrides. Enterprise deployments add an admin-controlled managed settings file that takes precedence over all of them.

How do Claude Code settings files merge?

They layer, with admin-managed policy settings at the top, then local, project, and user settings. Several keys are honored only from managed settings. Use --setting-sources to control which layers a session loads.

How do I stop Claude Code reading my .env file?

Add a deny rule: {"permissions": {"deny": ["Read(./.env)", "Read(./.env.*)"]}}. deny rules take precedence over allow rules and over the permission mode, so this holds even in acceptEdits.

How do I avoid reinstalling node_modules in every worktree?

Set worktree.symlinkDirs to ["node_modules"] (plus .cache or .bin if useful). Nothing is symlinked by default — it has to be configured explicitly.

How do I check which settings are actually loaded?

Run `claude doctor`, which reads settings files in the current directory without a trust prompt. To test whether config is causing a problem, start with `claude --safe-mode`, which disables all customizations while leaving auth and built-in tools working.

Keep reading