Skip to main content
What this unit solvessettings.json is Claude Code’s behavioral control plane, governing tool permissions (allow / ask / deny), environment variables, hook mounts, and model selection. settings.local.json is the personal override layer that is never committed to version control, intended for local secret paths and individual preferences. Understanding the boundary between the two and their merge precedence keeps private key paths off git and prevents the common confusion of writing a deny rule that appears to do nothing. This unit provides copy-paste configuration snippets and a concrete path for verifying that a rule is actually in effect.

Learning objectives

  • Name the four areas settings.json covers (permissions / env / hooks / model) and give a concrete key-value example for each.
  • Distinguish settings.json (team-shared, committed to version control) from settings.local.json (personal override, gitignored) by use-case.
  • Describe the merge precedence among user-level (~/.claude/settings.json), project-level (.claude/settings.json), local (.claude/settings.local.json), and the managed enterprise layer.
  • Configure a deny list protecting ~/.ssh, **/.env, and similar sensitive paths, and verify the rules are actually in effect.
  • Explain the permission-range differences among the six permission modes (default, acceptEdits, plan, auto, dontAsk, bypassPermissions) and choose the right one for a given task.

1. What settings.json does: four areas

settings.json is a structured JSON configuration file that Claude Code reads at startup to determine its own behavior. Its top-level keys are numerous — as of 2026-05 the official reference lists nearly a hundred, from cleanupPeriodDays to statusLine to outputStyle [1] — but in practice the ones you will actually touch fall into four areas:
  • permissions: tool permission rules (allow / ask / deny arrays), covered in depth in Section 4. This is the most important area of settings.json.
  • env: environment variables injected into every session.
  • hooks: custom commands mounted on lifecycle events (PreToolUse / PostToolUse / Stop, etc.); the mechanics are covered in 04-6.
  • model: the default model, accepting aliases ("sonnet" / "opus" / "haiku") or a full model ID. The complete list of available models depends on your provider (Anthropic API, Bedrock, Vertex AI); the settings documentation itself does not enumerate them (as of 2026-05) [1].
A minimal example that uses all four areas:
{
  "model": "opus",
  "env": {
    "ANTHROPIC_LOG": "info"
  },
  "permissions": {
    "deny": ["Read(~/.ssh/**)", "Read(.env)"],
    "ask": ["Bash(git push:*)"]
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": ".claude/hooks/format.sh" }]
      }
    ]
  }
}
settings.json is policy; CLAUDE.md is rulesThe division of labor is clear: permission rules are enforced by Claude Code, not by the model. Your prompt or CLAUDE.md shapes “what Claude will try to do,” but it does not change “what Claude Code permits” [2]. In other words, settings.json is policy (machine-enforced; the model cannot bypass it), while CLAUDE.md is rules (natural-language guidance the model reads, which it may not follow). To block a dangerous action, writing in CLAUDE.md asking the model not to do it is not sufficient — use permissions.deny.

2. settings.local.json: the personal override layer that stays out of version control

settings.local.json lives alongside settings.json in the .claude/ directory and is structurally identical. The single difference: it should never be committed to version control. The official documentation positions it as the gitignored local scope [1]. Typical uses are settings that are “valid only on your machine and should not be shared with the team”:
  • Environment variables pointing to local secret paths or personal accounts.
  • Extra deny rules you want personally but do not want to impose on everyone.
  • Hooks that call tools that exist only on your machine.
The decision criterion is simple: anything the whole team should apply goes in settings.json (committed); anything that belongs only to your machine goes in settings.local.json (not committed).
Do not let settings.local.json slip into gitWhen Claude Code first creates settings.local.json it usually adds it to the project .gitignore, but you should not assume this always happens. The hands-on exercise below gives you a git ls-files command to verify. Once this file — along with any secret paths inside it — is committed and pushed, the information is exposed, and git history is very hard to clean completely.

3. Merge precedence: four layers plus the managed enterprise layer

When the same setting appears in multiple scopes, Claude Code applies them in order of precedence. The official precedence, from highest to lowest [1]:
1

Managed (enterprise layer, highest)

Cannot be overridden by any other layer, including command-line arguments.
2

Command-line arguments

Temporary overrides for the current session.
3

Local (.claude/settings.local.json)

Overrides project and user.
4

Project (.claude/settings.json)

Overrides user.
5

User (~/.claude/settings.json, lowest)

Takes effect only when no other layer specifies the setting.
File locations for each scope (as of 2026-05) [1]:
ScopeLocation
ManagedmacOS /Library/Application Support/ClaudeCode/, Linux/WSL /etc/claude-code/, Windows C:\Program Files\ClaudeCode\ (or via plist / registry)
User~/.claude/settings.json
Project.claude/settings.json
Local.claude/settings.local.json (gitignored)
permissions merges — it does not overrideMost settings follow “later layer overrides earlier layer” semantics, but permissions is an exception: permission rules are merged across scopes, not replaced [1]. (This is the concrete instance of the “merge vs. override” distinction covered in 02-1.) More critically, deny rules are irreversible: once any layer denies an action, no other layer can allow it back. Deny rules are evaluated before allow across all scopes, so a deny in the user layer will block an allow in the project layer [2]. This corrects a common misconception: the belief that a local or project allow can lift an upper-layer deny. It cannot. The only way to lift a deny is to remove the deny rule.

4. permissions in practice: rule syntax and a baseline deny list

The rule format is Tool or Tool(specifier). Evaluation order is deny, then ask, then allow; the first matching rule wins, so deny always takes precedence [2]. One detail worth memorizing: a bare tool name (e.g. Bash) used as a deny rule removes the entire tool from the model’s context — the model cannot see it at all. A rule with a specifier (e.g. Bash(rm *)) keeps the tool visible and blocks only the matching calls [2]. Specifier syntax by tool (as of 2026-05) [2]:
  • Bash: Bash(npm run build) is an exact match; Bash(npm run *) is a prefix match; * may appear anywhere (Bash(* install) matches anything ending with install). Whitespace determines word boundaries: Bash(ls *) matches ls -la but not lsof, while Bash(ls*) matches both. The :* suffix is equivalent to a trailing *. For compound commands separated by &&, ||, ;, or |, each segment must independently match a rule before the call is allowed.
  • Read / Edit: follow gitignore conventions. Four path anchors to keep straight:
PatternMeaningExample
//pathFilesystem absolute pathRead(//Users/alice/secrets/**)
~/pathHome directoryRead(~/.ssh/**)
/pathRelative to project root (not absolute)Edit(/src/**/*.ts)
path or ./pathRelative to current directoryRead(.env)
The most common mistake: /Users/alice/file is not an absolute path — it is relative to the project root. Absolute paths require //Users/alice/file (two leading slashes) [2]. A bare filename follows gitignore semantics, so Read(.env) is equivalent to Read(**/.env) and matches .env at any depth.
  • WebFetch: WebFetch(domain:example.com).
  • MCP: mcp__server (entire server), mcp__server__*, mcp__server__tool (single tool).
  • Agent: Agent(Explore) controls available sub-agents.
A copy-paste baseline deny list (Windows-friendly)On Windows + PowerShell, two platform-specific details apply: Claude Code normalizes paths to POSIX form (C:\Users\alice becomes /c/Users/alice), so matching .env across drives requires //**/.env. PowerShell rules use the PowerShell(...) prefix and follow the same shape as Bash rules [2].
{
  "permissions": {
    "deny": [
      "Read(~/.ssh/**)",
      "Read(~/.aws/**)",
      "Read(//**/.env)",
      "Read(**/.env)",
      "Read(**/*.pem)",
      "Bash(curl:*)",
      "Bash(rm -rf:*)",
      "PowerShell(Remove-Item *)"
    ],
    "ask": [
      "Bash(git push:*)",
      "PowerShell(git push *)"
    ]
  }
}
This list blocks file reads of sensitive paths and dangerous commands, puts the irreversible git push behind an ask, and lets everything else pass through the default. This is the principle of least privilege from 03-3 applied concretely.
On restricting network access with permission rules: the official documentation explicitly warns that a rule like Bash(curl http://github.com/ *) is fragile — switching protocols, adding a redirect, or using a variable all bypass it. A more reliable approach is to deny curl, wget, and other network-capable Bash tools outright, and use WebFetch(domain:...) allowlisting instead. Alternatively, a PreToolUse hook can validate the URL before it runs. True OS-level blocking requires a sandbox [2]. Permissions and the sandbox are complementary layers: permissions block “what Claude attempts to access”; the sandbox blocks at the OS level “even if a prompt injection bypasses Claude’s judgment” (see 01-6).

Interactive rule evaluator

The evaluator below lets you try it directly: add rules, enter a tool call, and watch deny, ask, allow first-match-wins play out. The matching rule is highlighted in the verdict.

5. Permission modes: six authorization tiers

The rules in Section 4 are per-call authorization. Permission modes are the overall baseline: they control whether each tool call pauses to ask you by default. The mode sets the baseline; the allow / ask / deny rules from Section 4 layer on top. As of 2026-06, Claude Code has six modes, arranged from tightest to most permissive by “what gets through without asking” [5]:
ModeAuto-allowedTypical use
defaultReads onlyGetting started, sensitive work; every action reviewed
acceptEditsReads + file edits + common filesystem commands (mkdir / touch / rm / rmdir / mv / cp / sed)Iterative coding where you review with git diff after the fact
planReads only (research and planning only, no file changes)Mapping a codebase before touching it
autoNearly everything, but each action passes a background safety classifierLong tasks, reducing prompt fatigue
dontAskOnly pre-allowed tools and read-only Bash commands (everything else that would normally ask is rejected)Locked-down CI and scripts
bypassPermissionsEverything, including safety checksIsolated containers / VMs only
Three invariants hold in every mode [5]:
  • Deny rules and explicit ask rules are enforced in every mode, including bypassPermissions (allow rules become meaningless under bypass since everything passes through anyway). To hard-block an action, write a deny rule — do not rely on choosing a mode.
  • Writes to protected paths are never auto-allowed except under bypassPermissions. Paths like .git, .claude (except .claude/worktrees), .vscode, .idea, .npmrc, .mcp.json, .claude.json, and shell rc files (.bashrc / .zshrc, etc.) are intercepted even if you have acceptEdits active or an Edit(.claude/**) allow rule in settings. This security check runs before allow rules [5]; see the official protected-paths section [5] for the full list.
  • Modes are a baseline only. The rules from Section 4 always layer on top — they are not mutually exclusive.

Switching modes

Three ways [5]:
  • Within a session: Shift+Tab cycles default -> acceptEdits -> plan. auto, bypassPermissions, and dontAsk are not in the default cycle; they require separate activation (auto requires account eligibility; dontAsk never enters the cycle and is only activated via flag).
  • At startup: claude --permission-mode plan (or any other mode name).
  • As the default: set permissions.defaultMode in settings.json.
{
  "permissions": {
    "defaultMode": "acceptEdits"
  }
}

The trap in three modes

auto, dontAsk, and bypassPermissions each carry a specific misread worth addressing separately. auto (requires v2.1.83+) is not “automatic yes” — it is “classifier blocks high-risk, rest passes through.” An independent classifier model reviews each action before it runs, blocking actions that exceed the scope of your request, reach unknown infrastructure, or appear to be driven by malicious content. curl | bash, production deployments, force pushes to main, and irreversible deletion of existing files are all blocked by default [5]. Two behaviors you need to know: conversation-level boundaries you state (“do not push yet”, “wait until I review”) are treated as block signals by the classifier, but those boundaries live in the transcript only — once context compression removes that sentence, the boundary is gone. For a hard guarantee, use a deny rule. Also, defaultMode: "auto" in a project-layer or local-layer file is silently ignored (v2.1.142+) to prevent a repo from granting itself auto mode; this setting can only be placed in ~/.claude/settings.json [5]. dontAsk is “automatic rejection,” not “automatic approval.” The name is easy to misread: it rejects all calls that would normally prompt for confirmation, leaving only tools matching your permissions.allow and read-only Bash commands. Even ask rules are rejected outright rather than prompted [5]. This mode is for CI where you have already precisely defined what Claude is allowed to do; anything not pre-allowed is silently refused. bypassPermissions disables safety checks and should only be used in isolated environments. It allows writes to protected paths (from v2.1.126), removing all safety nets. Only explicit ask rules and rm -rf / / rm -rf ~ (filesystem root / home deletions, the last circuit breaker) still block [5]. On Linux / macOS, starting as root or with sudo is rejected outright. The official documentation is unambiguous: it provides zero protection against prompt injection; use auto instead when the goal is “fewer interruptions with some protection” [5]. --dangerously-skip-permissions is this mode; the name is instructive.
Same task, which mode?You want Claude to refactor a module into multiple files, which means many file writes, but you want to keep review authority:
  • default: every write pauses for confirmation — safe, but you will be interrupted dozens of times.
  • acceptEdits: writes proceed without interruption; one git diff after the run covers everything. Writes outside the working directory, protected paths, and other Bash commands still ask. This is the right choice for most “I will review after” scenarios.
  • bypassPermissions: even protected paths are not blocked. Unless you are in a throwaway container, this trades reproducibility and safety for convenience at an unfavorable ratio.
The decision rule: willing to review the diff after (not step by step) — use acceptEdits. Need to review every step — use default. Need a long unattended run with some protection — use auto (in an isolated or low-risk repo). bypass belongs only in environments where “even if a prompt injection takes over, it cannot reach anything I care about.”
The managed layer can lock high-risk modesEnterprise managed settings can use permissions.disableBypassPermissionsMode: "disable" to lock out bypass, and permissions.disableAutoMode: "disable" to lock out auto [5]. In a team environment, if you find a mode switch is not taking effect, check whether the managed layer is blocking it — it overrides CLI flags.

6. How settings.json and claude.json divide the work

One sentence: settings.json owns policy (behavioral settings you edit), while claude.json owns state (internal state Claude Code maintains itself, such as per-project history and trust records). You should not manually edit claude.json — it is not a human-authored file. Its correct path, content format, and the question of whether to touch it at all are covered in 02-4.

Tool comparison

The concept of a “project-level behavioral configuration file” exists across tools, but maturity and granularity vary significantly (as of 2026-05; exact formats are subject to each vendor’s current documentation):
ConceptAnthropic Claude (primary)OpenAI (Codex)Google (Gemini CLI)GitHub CopilotCursor
Project-level config file.claude/settings.json [1].codex/config.toml (project root down, closest wins) + AGENTS.md [3].gemini/settings.json [4].github/copilot-instructions.md (instructions, not permissions).cursor/rules/*.mdc + GUI settings
Personal override (not in VCS).claude/settings.local.json [1]Needs source verificationNeeds source verificationNeeds source verificationNeeds source verification
User-global config~/.claude/settings.json [1]~/.codex/config.toml [3]~/.gemini/settings.json [4]VS Code user settingsUser Rules (GUI)
Tool permission control (allow / deny)permissions.allow / deny / ask arrays [2]approval_policy / sandbox_mode in config.toml [3]settings.json includes tool permissions [4]No equivalent fine-grained tool permissionsNeeds source verification
Hook mount pointshooks.PreToolUse / PostToolUse / Stop [1]Needs source verification (no direct equivalent)Needs source verificationNo direct equivalentNo direct equivalent
The comparison table gives coordinates, not detailsExact mechanisms and paths for each cell are fast-moving facts; entries that cannot be confirmed are marked “needs source verification” — refer to each vendor’s official documentation. Fine-grained, machine-enforced tool permissions (as opposed to natural-language instructions) are most mature in Claude Code’s permissions and Codex’s sandbox / approval policy. Copilot and Cursor project files are closer to “instruction / rule” artifacts that rely on model compliance rather than hard enforcement boundaries.

Hands-on exercises

1

Add the baseline deny list and verify it is actually in effect

Add the baseline deny list above to your project’s .claude/settings.json, then take two steps to confirm the rules are working rather than just present:Run /permissions inside Claude Code. This UI lists all merged rules and which settings file each one came from — you can see directly whether the deny rules made it into the final policy [2].Probe it: ask Claude to read a path that is denied (for example, a file under ~/.ssh/), and confirm it is blocked rather than read. Remember you are verifying the merged final policy, not a single settings file.
2

Create settings.local.json with a personal variable and confirm it is not tracked by git

Create .claude/settings.local.json with an env variable that belongs only to your machine, then verify it is gitignored:
git ls-files .claude/settings.local.json   # should return empty (untracked)
If the filename comes back, the file is already tracked. Run git rm --cached .claude/settings.local.json immediately and add the pattern to .gitignore.

Common pitfalls

Anti-pattern list
  • Assuming a local or project allow can lift an upper-layer deny: it cannot. A deny in any layer blocks; deny is evaluated before allow across all scopes [2]. To unblock something, remove the deny rule — do not add an allow on top.
  • Treating the deny list as the only security layer: deny rules apply only to Claude’s built-in file tools and the Bash file commands it can recognize (cat, head, etc.). A Python or Node script that opens a file directly is not blocked. OS-level blocking of all processes requires a sandbox [2].
  • Storing secrets as plain text in env: env is injected into every session. An API key in plain text there is effectively a secret left in a config file. Use apiKeyHelper / awsCredentialExport-style helper scripts to generate credentials dynamically, or point to a secret manager.
  • Manually editing claude.json: that file is Claude Code’s own state store, not a policy file (see 02-4). To change behavior, edit settings.json.
  • Treating settings.local.json as “highest priority”: it does override project and user, but it cannot override the managed enterprise layer or deny rules. It is an additive relationship with settings.json, not a replacement.
  • Using mode selection to block dangerous actions: modes set a baseline; deny rules are the hard wall, and they are enforced in every mode including bypassPermissions. Relying on bypassPermissions to save effort is equivalent to removing all safety nets. Use auto when the goal is fewer interruptions with protection still in place (Section 5).

Self-check

The bar for passing this unit
  1. Can you explain in one sentence the division of labor between settings.json (policy, machine-enforced) and CLAUDE.md (rules, the model may not follow)? To block a dangerous action, which file do you write in?
  2. Does your project .gitignore exclude settings.local.json? Does git ls-files .claude/settings.local.json return empty?
  3. Given the rule Read(/Users/alice/secret), does it block an absolute path or a path relative to the project root? How would you write it to block the absolute path?
  4. Is your current set of active deny rules something you know from looking at a single settings file, or from reading the merged final policy with /permissions?
  5. The user layer has an allow rule; the project layer has a deny for the same action. Is the action ultimately allowed or blocked?
  6. Does dontAsk mode automatically allow actions that would normally prompt, or automatically reject them? What is the fundamental difference between bypassPermissions and auto?

Sources and further reading

Factual claims are grounded in official documentation; fast-changing items are annotated as of 2026-05 (the permission modes section is annotated as of 2026-06).
  • [1] Anthropic, “Claude Code settings,” Claude Code Docs. (settings.json top-level key reference; four-layer plus managed precedence Managed -> CLI args -> Local -> Project -> User; permissions merge across scopes rather than override; model key accepts aliases and full IDs) https://code.claude.com/docs/en/settings (as of 2026-05)
  • [2] Anthropic, “Configure permissions,” Claude Code Docs. (rule syntax Tool(specifier); evaluation order deny -> ask -> allow first-match-wins; a deny in any layer cannot be lifted by an allow in another; Read / Edit gitignore path anchors and Windows POSIX normalization; permissions enforced by Claude Code not the model; /permissions to inspect merged rules; permissions and sandbox as complementary layers) https://code.claude.com/docs/en/permissions (as of 2026-05)
  • [3] OpenAI, “Codex configuration,” OpenAI Developers Docs. (user-global ~/.codex/config.toml and project .codex/config.toml (project root down, closest wins); approval_policy / sandbox_mode; AGENTS.md as Codex project instructions equivalent to CLAUDE.md) https://developers.openai.com/codex/config-reference (as of 2026-05)
  • [4] Google, “Gemini CLI configuration,” Gemini CLI Docs. (user ~/.gemini/settings.json and project .gemini/settings.json, project overrides user; settings.json controls tool permissions, MCP servers, workspace, etc.) https://geminicli.com/docs/reference/configuration/ (as of 2026-05)
  • [5] Anthropic, “Choose a permission mode,” Claude Code Docs. (six modes default / acceptEdits / plan / auto / dontAsk / bypassPermissions and their auto-allow scopes; Shift+Tab cycling and --permission-mode flag, permissions.defaultMode; deny and explicit ask rules enforced in every mode; protected paths not auto-allowed except under bypass; auto background classifier, requires v2.1.83+, conversation boundaries lost on context compression, defaultMode: auto ignored in project/local layers; dontAsk auto-rejects; bypassPermissions disables safety checks, root/sudo refused, disableBypassPermissionsMode / disableAutoMode managed locks) https://code.claude.com/docs/en/permission-modes (as of 2026-06)
  • Related: 02-1 on the configuration layer model and “merge vs. override”; 02-4 on claude.json as a state file; 01-6 on where permission boundaries sit in harness design; 03-3 on least privilege and supply-chain risk; 04-6 on using hooks for deterministic approval gates.