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
| Scope | Path | Use it for |
|---|---|---|
| User | ~/.claude/settings.json | Your defaults across every project — statusline, personal hooks, model preference. |
| Project | .claude/settings.json in the repo | Shared, committed config: permission rules and hooks everyone on the team should get. |
| Local | .claude/settings.local.json in the repo | Your personal overrides for that project. Gitignore it. |
| Managed / policy | Admin-controlled location | Enterprise 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.
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": ["Bash(npm *)", "Edit(.claude)", "Read"],
"ask": ["Edit(//etc/*)"],
"deny": ["Bash(rm -rf *)"],
"additionalDirectories": ["../shared-types"],
"disableBypassPermissionsMode": "disable"
}
}defaultMode— one ofacceptEdits,auto,bypassPermissions,manual,dontAsk,plan. What each does.allow/ask/deny— arrays ofTool(specifier)rules. Exact (Bash(npm run test)), wildcard (Bash(git *)), path glob (Edit(docs/**)), or a bare tool name.denytakes 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.
{
"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
{
"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
{
"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:
{
"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 suggestsnode_modules,.cache,.bin.branchFrom—fresh(default) branches fromorigin/<default-branch>for a clean tree;headbranches 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
{
"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-persistencedisables transcript writes entirely.attribution— attribution text for commits and PRs; an empty string hides it. This replaces the deprecatedincludeCoAuthoredBy.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:
// .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:
// ~/.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
Claude Code permission modes, explained
The six modes the CLI accepts, what each one stops asking about, and how to pick one for unattended runs.
Claude Code hooks: a practical guide
All thirteen hook events, the stdin/stdout contract, and scripts you can paste in today.
Building a custom Claude Code statusline
The complete stdin JSON schema (context window, rate limits, PR, worktree) and a script that uses it.