> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 02-3 settings.json and settings.local.json

> settings.json is Claude Code's behavioral control plane, governing tool permissions, environment variables, hooks, and model selection. settings.local.json is the personal override layer that stays out of version control. Understanding the boundary between the two and their merge precedence keeps private keys off git and prevents confusion when a deny rule appears to do nothing.

export const PermissionEval = ({lang = "zh"}) => {
  const t = ({
    zh: {
      title: "權限規則評估器",
      subtitle: "拖曳規則、選工具，看 deny → ask → allow 優先序的裁定結果",
      denyLabel: "Deny 規則（最高優先）",
      askLabel: "Ask 規則",
      allowLabel: "Allow 規則",
      toolLabel: "模擬工具呼叫",
      evalBtn: "評估",
      resetBtn: "重置",
      addRuleBtn: "＋ 新增規則",
      removeBtn: "移除",
      verdict: {
        deny: "DENY",
        ask: "ASK",
        allow: "ALLOW",
        default: "ALLOW（預設放行）"
      },
      verdictLabel: "裁定結果",
      matchedLabel: "命中規則",
      noMatch: "無規則命中，走預設放行",
      evalOrder: "評估順序：deny → ask → allow，第一條命中的規則勝出",
      toolPlaceholder: "例：Bash(git push origin main)",
      rulePlaceholder: "例：Bash(git push:*)",
      bareNote: "裸工具名（如 Bash）命中後，模型完全看不到該工具",
      denyColor: "#c0392b",
      askColor: "#d68910",
      allowColor: "#1a7a4a",
      defaultColor: "#1a7a4a"
    },
    en: {
      title: "Permission Rule Evaluator",
      subtitle: "Add rules, enter a tool call, and watch deny -> ask -> allow first-match-wins play out",
      denyLabel: "Deny rules (highest priority)",
      askLabel: "Ask rules",
      allowLabel: "Allow rules",
      toolLabel: "Simulated tool call",
      evalBtn: "Evaluate",
      resetBtn: "Reset",
      addRuleBtn: "+ Add rule",
      removeBtn: "Remove",
      verdict: {
        deny: "DENY",
        ask: "ASK",
        allow: "ALLOW",
        default: "ALLOW (default pass-through)"
      },
      verdictLabel: "Verdict",
      matchedLabel: "Matched rule",
      noMatch: "No rule matched; default pass-through applies",
      evalOrder: "Evaluation order: deny -> ask -> allow; the first matching rule wins",
      toolPlaceholder: "e.g. Bash(git push origin main)",
      rulePlaceholder: "e.g. Bash(git push:*)",
      bareNote: "A bare tool name (e.g. Bash) hides the entire tool from the model when denied",
      denyColor: "#c0392b",
      askColor: "#d68910",
      allowColor: "#1a7a4a",
      defaultColor: "#1a7a4a"
    }
  })[lang];
  const defaultState = () => ({
    deny: ["Read(~/.ssh/**)", "Read(**/.env)", "Bash(rm -rf:*)"],
    ask: ["Bash(git push:*)"],
    allow: [],
    tool: "Bash(git push origin main)",
    result: null
  });
  const [state, setState] = useState(defaultState());
  const [newRule, setNewRule] = useState({
    deny: "",
    ask: "",
    allow: ""
  });
  const matchRule = (rule, toolCall) => {
    const bare = !rule.includes("(");
    if (bare) {
      const toolName = toolCall.split("(")[0].trim();
      return rule.trim() === toolName;
    }
    const m = rule.match(/^(\w+)\((.+)\)$/);
    if (!m) return false;
    const [, rTool, rSpec] = m;
    const callMatch = toolCall.match(/^(\w+)\((.+)\)$/);
    if (!callMatch) {
      return rTool === toolCall.trim() && bare;
    }
    const [, cTool, cArg] = callMatch;
    if (rTool !== cTool) return false;
    if (rSpec === "*") return true;
    if (rSpec.endsWith(":*")) {
      return cArg.startsWith(rSpec.slice(0, -2));
    }
    if (rSpec.endsWith("*")) {
      return cArg.startsWith(rSpec.slice(0, -1));
    }
    if (rSpec.startsWith("*")) {
      return cArg.endsWith(rSpec.slice(1));
    }
    return cArg === rSpec || cArg.startsWith(rSpec + " ");
  };
  const evaluate = () => {
    const tool = state.tool.trim();
    if (!tool) return;
    for (const rule of state.deny) {
      if (matchRule(rule, tool)) {
        setState(s => ({
          ...s,
          result: {
            verdict: "deny",
            rule
          }
        }));
        return;
      }
    }
    for (const rule of state.ask) {
      if (matchRule(rule, tool)) {
        setState(s => ({
          ...s,
          result: {
            verdict: "ask",
            rule
          }
        }));
        return;
      }
    }
    for (const rule of state.allow) {
      if (matchRule(rule, tool)) {
        setState(s => ({
          ...s,
          result: {
            verdict: "allow",
            rule
          }
        }));
        return;
      }
    }
    setState(s => ({
      ...s,
      result: {
        verdict: "default",
        rule: null
      }
    }));
  };
  const addRule = bucket => {
    const val = newRule[bucket].trim();
    if (!val) return;
    setState(s => ({
      ...s,
      [bucket]: [...s[bucket], val]
    }));
    setNewRule(r => ({
      ...r,
      [bucket]: ""
    }));
  };
  const removeRule = (bucket, idx) => {
    setState(s => ({
      ...s,
      [bucket]: s[bucket].filter((_, i) => i !== idx)
    }));
  };
  const verdictColor = state.result ? state.result.verdict === "deny" ? t.denyColor : state.result.verdict === "ask" ? t.askColor : t.allowColor : "transparent";
  const buckets = [{
    key: "deny",
    label: t.denyLabel,
    color: t.denyColor
  }, {
    key: "ask",
    label: t.askLabel,
    color: t.askColor
  }, {
    key: "allow",
    label: t.allowLabel,
    color: t.allowColor
  }];
  const css = `
    .pe-root {
      --pe-bg: #faf8f4;
      --pe-border: #d6cfc4;
      --pe-text: #2a2218;
      --pe-muted: #7a7060;
      --pe-input-bg: #fff;
      --pe-sienna: #bf7551;
      font-family: inherit;
      background: var(--pe-bg);
      border: 1px solid var(--pe-border);
      border-radius: 10px;
      padding: 1.2rem 1.4rem 1.4rem;
      margin: 1.25rem 0;
    }
    .dark .pe-root {
      --pe-bg: #1e1c18;
      --pe-border: #3a3628;
      --pe-text: #e8e0d0;
      --pe-muted: #9a9080;
      --pe-input-bg: #2a2820;
    }
    .pe-title {
      font-size: 1rem;
      font-weight: 600;
      color: var(--pe-sienna);
      margin: 0 0 0.2rem;
    }
    .pe-sub {
      font-size: 0.82rem;
      color: var(--pe-muted);
      margin: 0 0 1rem;
    }
    .pe-buckets {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 0.75rem;
      margin-bottom: 1rem;
    }
    @media (max-width: 640px) {
      .pe-buckets { grid-template-columns: 1fr; }
    }
    .pe-bucket {
      border: 1px solid var(--pe-border);
      border-radius: 7px;
      padding: 0.7rem 0.8rem;
      background: var(--pe-input-bg);
      min-width: 0;
    }
    .pe-bucket-label {
      font-size: 0.78rem;
      font-weight: 600;
      margin-bottom: 0.5rem;
    }
    .pe-rule-row {
      display: flex;
      align-items: center;
      gap: 0.35rem;
      margin-bottom: 0.3rem;
      min-width: 0;
    }
    .pe-rule-tag {
      flex: 1;
      min-width: 0;
      font-size: 0.75rem;
      font-family: monospace;
      background: var(--pe-bg);
      border: 1px solid var(--pe-border);
      border-radius: 4px;
      padding: 0.18rem 0.4rem;
      color: var(--pe-text);
      word-break: break-all;
      overflow-wrap: break-word;
    }
    .pe-rule-tag.pe-highlighted {
      outline: 2px solid currentColor;
      font-weight: 700;
    }
    .pe-rm-btn {
      background: none;
      border: none;
      color: var(--pe-muted);
      cursor: pointer;
      font-size: 0.85rem;
      padding: 0 0.2rem;
      line-height: 1;
    }
    .pe-rm-btn:hover { color: #c0392b; }
    .pe-add-row {
      display: flex;
      gap: 0.3rem;
      margin-top: 0.4rem;
      min-width: 0;
    }
    .pe-add-input {
      flex: 1 1 0;
      min-width: 0;
      width: 0;
      font-size: 0.75rem;
      font-family: monospace;
      padding: 0.2rem 0.4rem;
      border: 1px solid var(--pe-border);
      border-radius: 4px;
      background: var(--pe-bg);
      color: var(--pe-text);
    }
    .pe-add-input:focus { outline: 1.5px solid var(--pe-sienna); }
    .pe-add-btn {
      font-size: 0.72rem;
      background: none;
      border: 1px solid var(--pe-border);
      border-radius: 4px;
      padding: 0.2rem 0.45rem;
      cursor: pointer;
      color: var(--pe-muted);
      white-space: nowrap;
    }
    .pe-add-btn:hover { border-color: var(--pe-sienna); color: var(--pe-sienna); }
    .pe-tool-row {
      display: flex;
      gap: 0.5rem;
      align-items: center;
      margin-bottom: 0.9rem;
      min-width: 0;
    }
    .pe-tool-label {
      font-size: 0.82rem;
      color: var(--pe-muted);
      white-space: nowrap;
      flex-shrink: 0;
    }
    .pe-tool-input {
      flex: 1 1 0;
      min-width: 0;
      width: 0;
      font-family: monospace;
      font-size: 0.82rem;
      padding: 0.3rem 0.6rem;
      border: 1px solid var(--pe-border);
      border-radius: 5px;
      background: var(--pe-input-bg);
      color: var(--pe-text);
    }
    .pe-tool-input:focus { outline: 1.5px solid var(--pe-sienna); }
    .pe-btns {
      display: flex;
      gap: 0.5rem;
      margin-bottom: 0.9rem;
    }
    .pe-eval-btn {
      background: var(--pe-sienna);
      color: #fff;
      border: none;
      border-radius: 5px;
      padding: 0.35rem 1rem;
      font-size: 0.85rem;
      cursor: pointer;
      font-weight: 600;
    }
    .pe-eval-btn:hover { background: #a85f3f; }
    .pe-reset-btn {
      background: none;
      border: 1px solid var(--pe-border);
      border-radius: 5px;
      padding: 0.35rem 0.8rem;
      font-size: 0.82rem;
      cursor: pointer;
      color: var(--pe-muted);
    }
    .pe-reset-btn:hover { border-color: var(--pe-sienna); color: var(--pe-sienna); }
    .pe-result {
      border-radius: 7px;
      padding: 0.75rem 1rem;
      border: 1.5px solid;
      margin-bottom: 0.6rem;
    }
    .pe-verdict-row {
      display: flex;
      align-items: center;
      gap: 0.6rem;
      margin-bottom: 0.3rem;
    }
    .pe-verdict-badge {
      font-weight: 700;
      font-size: 0.95rem;
      letter-spacing: 0.04em;
      font-family: monospace;
    }
    .pe-verdict-label {
      font-size: 0.78rem;
      color: var(--pe-muted);
    }
    .pe-matched-rule {
      font-family: monospace;
      font-size: 0.78rem;
      color: var(--pe-text);
    }
    .pe-note {
      font-size: 0.75rem;
      color: var(--pe-muted);
      margin-top: 0.65rem;
    }
    .pe-order-note {
      font-size: 0.76rem;
      color: var(--pe-muted);
      border-top: 1px solid var(--pe-border);
      padding-top: 0.55rem;
      margin-top: 0.4rem;
    }
  `;
  return <div className="pe-root">
      <style>{css}</style>
      <p className="pe-title">{t.title}</p>
      <p className="pe-sub">{t.subtitle}</p>

      <div className="pe-buckets">
        {buckets.map(({key, label, color}) => <div className="pe-bucket" key={key}>
            <div className="pe-bucket-label" style={{
    color
  }}>{label}</div>
            {state[key].map((rule, idx) => {
    const highlighted = state.result && state.result.rule === rule && state.result.verdict === key;
    return <div className="pe-rule-row" key={idx}>
                  <span className={"pe-rule-tag" + (highlighted ? " pe-highlighted" : "")} style={highlighted ? {
      color
    } : {}}>
                    {rule}
                  </span>
                  <button className="pe-rm-btn" onClick={() => removeRule(key, idx)} title={t.removeBtn}>
                    ×
                  </button>
                </div>;
  })}
            <div className="pe-add-row">
              <input className="pe-add-input" placeholder={t.rulePlaceholder} value={newRule[key]} onChange={e => setNewRule(r => ({
    ...r,
    [key]: e.target.value
  }))} onKeyDown={e => e.key === "Enter" && addRule(key)} />
              <button className="pe-add-btn" onClick={() => addRule(key)}>
                {t.addRuleBtn}
              </button>
            </div>
          </div>)}
      </div>

      <div className="pe-tool-row">
        <span className="pe-tool-label">{t.toolLabel}：</span>
        <input className="pe-tool-input" placeholder={t.toolPlaceholder} value={state.tool} onChange={e => setState(s => ({
    ...s,
    tool: e.target.value,
    result: null
  }))} onKeyDown={e => e.key === "Enter" && evaluate()} />
      </div>

      <div className="pe-btns">
        <button className="pe-eval-btn" onClick={evaluate}>
          {t.evalBtn}
        </button>
        <button className="pe-reset-btn" onClick={() => {
    setState(defaultState());
    setNewRule({
      deny: "",
      ask: "",
      allow: ""
    });
  }}>
          {t.resetBtn}
        </button>
      </div>

      {state.result && <div className="pe-result" style={{
    borderColor: verdictColor,
    background: verdictColor + "18"
  }}>
          <div className="pe-verdict-row">
            <span className="pe-verdict-badge" style={{
    color: verdictColor
  }}>
              {t.verdict[state.result.verdict]}
            </span>
            <span className="pe-verdict-label">{t.verdictLabel}</span>
          </div>
          <div className="pe-matched-rule">
            {state.result.rule ? `${t.matchedLabel}：${state.result.rule}` : t.noMatch}
          </div>
        </div>}

      <p className="pe-note">{t.bareNote}</p>
      <p className="pe-order-note">{t.evalOrder}</p>
    </div>;
};

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="en"
  items={[
"Writing in CLAUDE.md asking the model not to do something is not a guarantee -- the model can miss it. Use permissions.deny to enforce hard blocks.",
"Did you write that deny rule correctly? Use /permissions to see the merged policy, not just a single settings file.",
"/Users/alice/file is not an absolute path -- it is relative to the project root. Absolute paths need //Users/alice/file (two leading slashes).",
"Storing an API key in plain text inside env is the same as leaving a secret in a config file that rides every session.",
"'permissions merges, it does not override' -- memorize this. A deny in any layer blocks; no other layer's allow can lift it.",
"A lower-layer allow cannot unlock a higher-layer deny. To allow something, remove the deny rule -- do not add an allow on top.",
"settings.local.json slipping into git means your local secrets are now in the cloud. Verify with git ls-files.",
]}
/>

<Info>
  **What this unit solves**

  `settings.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.
</Info>

## 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](/en/code-agent/customization/hooks).
* **`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:

```json theme={null}
{
  "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" }]
      }
    ]
  }
}
```

<Tip>
  **settings.json is policy; CLAUDE.md is rules**

  The 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`.
</Tip>

## 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).**

<Warning>
  **Do not let settings.local.json slip into git**

  When 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.
</Warning>

## 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]:

<Steps>
  <Step title="Managed (enterprise layer, highest)">
    Cannot be overridden by any other layer, including command-line arguments.
  </Step>

  <Step title="Command-line arguments">
    Temporary overrides for the current session.
  </Step>

  <Step title="Local (.claude/settings.local.json)">
    Overrides project and user.
  </Step>

  <Step title="Project (.claude/settings.json)">
    Overrides user.
  </Step>

  <Step title="User (~/.claude/settings.json, lowest)">
    Takes effect only when no other layer specifies the setting.
  </Step>
</Steps>

File locations for each scope (as of 2026-05) \[1]:

| Scope   | Location                                                                                                                                          |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Managed | macOS `/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)                                                                                                        |

<Warning>
  **permissions merges -- it does not override**

  Most 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](/en/code-agent/configuration/config-layer-model).) 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.
</Warning>

## 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:

| Pattern            | Meaning                                     | Example                          |
| ------------------ | ------------------------------------------- | -------------------------------- |
| `//path`           | Filesystem absolute path                    | `Read(//Users/alice/secrets/**)` |
| `~/path`           | Home directory                              | `Read(~/.ssh/**)`                |
| `/path`            | Relative to **project root** (not absolute) | `Edit(/src/**/*.ts)`             |
| `path` or `./path` | Relative to current directory               | `Read(.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.

<Note>
  **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].

  ```json theme={null}
  {
    "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](/en/code-agent/judgment/security-privacy-supply-chain) applied concretely.
</Note>

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](/en/code-agent/foundations/harness-engineering)).

### 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.

<PermissionEval lang="en" />

## 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]:

| Mode                | Auto-allowed                                                                                               | Typical use                                                      |
| ------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `default`           | Reads only                                                                                                 | Getting started, sensitive work; every action reviewed           |
| `acceptEdits`       | Reads + file edits + common filesystem commands (`mkdir` / `touch` / `rm` / `rmdir` / `mv` / `cp` / `sed`) | Iterative coding where you review with `git diff` after the fact |
| `plan`              | Reads only (research and planning only, no file changes)                                                   | Mapping a codebase before touching it                            |
| `auto`              | Nearly everything, but each action passes a background safety classifier                                   | Long tasks, reducing prompt fatigue                              |
| `dontAsk`           | Only pre-allowed tools and read-only Bash commands (everything else that would normally ask is rejected)   | Locked-down CI and scripts                                       |
| `bypassPermissions` | Everything, including safety checks                                                                        | Isolated 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`.

```json theme={null}
{
  "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.

<Note>
  **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."
</Note>

<Warning>
  **The managed layer can lock high-risk modes**

  Enterprise 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.
</Warning>

## 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](/en/code-agent/configuration/claude-json).

## 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):

| Concept                                | Anthropic Claude (primary)                       | OpenAI (Codex)                                                            | Google (Gemini CLI)                          | GitHub Copilot                                                    | Cursor                               |
| -------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------ |
| 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]               | Per official docs                                                         | Per official docs                            | Per official docs                                                 | Per official docs                    |
| User-global config                     | `~/.claude/settings.json` \[1]                   | `~/.codex/config.toml` \[3]                                               | `~/.gemini/settings.json` \[4]               | VS Code user settings                                             | User 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 permissions                       | Per official docs                    |
| Hook mount points                      | `hooks.PreToolUse` / `PostToolUse` / `Stop` \[1] | Per official docs (no direct equivalent)                                  | Per official docs                            | No direct equivalent                                              | No direct equivalent                 |

<Note>
  **The comparison table gives coordinates, not details**

  Exact mechanisms and paths for each cell are fast-moving facts; refer to each vendor's official documentation for current details. 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.
</Note>

## Hands-on exercises

<Steps>
  <Step title="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.
  </Step>

  <Step title="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:

    ```powershell theme={null}
    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`.
  </Step>
</Steps>

## Common pitfalls

<Warning>
  **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](/en/code-agent/configuration/claude-json)). 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).
</Warning>

## Self-check

<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`?
</Check>

## 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).

<div className="references">
  * \[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](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](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](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/](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](https://code.claude.com/docs/en/permission-modes) (as of 2026-06)
</div>

* Related: [02-1](/en/code-agent/configuration/config-layer-model) on the configuration layer model and "merge vs. override"; [02-4](/en/code-agent/configuration/claude-json) on claude.json as a state file; [01-6](/en/code-agent/foundations/harness-engineering) on where permission boundaries sit in harness design; [03-3](/en/code-agent/judgment/security-privacy-supply-chain) on least privilege and supply-chain risk; [04-6](/en/code-agent/customization/hooks) on using hooks for deterministic approval gates.
