> ## 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 與 settings.local.json

> settings.json 是 Claude Code 的行為控制面，管轄工具權限、環境變數、hooks 與模型選擇；settings.local.json 是不進版控的個人覆寫層。分清兩者用途邊界與合併優先序，才不會把私鑰推上 git，也不會困惑於 deny 規則明明寫了卻沒生效。

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="zh"
  items={[
"寫 CLAUDE.md 求模型別做某事，模型可能漏——要擋住危險動作得用 permissions.deny。",
"你 deny 寫對了沒？用 /permissions 看合併後的策略，不是看單一設定檔。",
"/Users/alice/file 不是絕對路徑，是相對專案根——絕對路徑要寫 //Users/alice/file（兩條斜線）。",
"env 明文存 API key，等於把密鑰留在設定檔裡，session 每次都帶著它。",
"「permissions 是合併不是覆寫」，背起來——任一層 deny 即擋，其他層 allow 解不開。",
"低層 allow 解不開高層的 deny，要放行就移除那條 deny，不是加 allow。",
"settings.local.json 漏進 git，本機密鑰就上雲了——用 git ls-files 確認它沒被追蹤。",
]}
/>

<Info>
  **這個單元解決什麼問題**

  `settings.json` 是 Claude Code 的行為控制面，管轄工具權限（allow / ask / deny）、環境變數、hooks 掛載與模型選擇；`settings.local.json` 是不進版控的個人覆寫層，放本機密鑰路徑或個人偏好。分清兩者的用途邊界與合併優先序，你才不會把私鑰路徑推上 git，也不會困惑於為什麼某條 deny 規則明明寫了卻沒生效。這個單元給你可貼用的設定片段與一條驗證規則是否真的生效的路徑。
</Info>

## 學習目標

* [ ] 能說出 `settings.json` 可設定的四大面向（permissions / env / hooks / model），並各舉一個實際鍵值範例。
* [ ] 能分清 `settings.json`（團隊共享、提交版控）與 `settings.local.json`（個人覆寫、gitignore）的用途邊界。
* [ ] 能描述使用者級（`~/.claude/settings.json`）、專案級（`.claude/settings.json`）、local（`.claude/settings.local.json`）與 managed 企業層的合併優先序。
* [ ] 能設定 deny list 保護 `~/.ssh`、`**/.env` 等敏感路徑，並驗證規則是否實際生效。
* [ ] 能說出六種 permission 模式（`default`、`acceptEdits`、`plan`、`auto`、`dontAsk`、`bypassPermissions`）的放行範圍差異，並依任務選對模式。

***

## 1. settings.json 能做什麼：四大面向

`settings.json` 是 JSON 格式的結構化設定檔，Claude Code 啟動時讀取它來決定自己的行為。它的頂層鍵很多（截至 2026-05 官方 reference 列出近百個，從 `cleanupPeriodDays` 到 `statusLine` 到 `outputStyle`）\[1]，但你日常會動到的集中在四個面向：

* **`permissions`**：工具權限規則（`allow` / `ask` / `deny` 三個陣列），第 4 節詳談。這是 settings.json 最重要的面向。
* **`env`**：注入到每個 session 的環境變數。
* **`hooks`**：在生命週期事件（`PreToolUse` / `PostToolUse` / `Stop` 等）掛載自訂指令，機制細節見 [04-6](/code-agent/customization/hooks)。
* **`model`**：指定預設模型，接受別名（`"sonnet"` / `"opus"` / `"haiku"`）或完整 model ID；完整可用清單依你的 provider（Anthropic API、Bedrock、Vertex AI）而定，settings 文件本身不列舉（截至 2026-05）\[1]。

一個四面向都用到的最小範例：

```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 是策略，CLAUDE.md 是規則**

  分工的根據很明確：權限規則由 Claude Code 強制執行，不是由模型執行；你的 prompt 或 `CLAUDE.md` 塑形「Claude 會嘗試做什麼」，但不改變「Claude Code 允許什麼」\[2]。換句話說 `settings.json` 是**策略**（machine-enforced，模型繞不過），`CLAUDE.md` 是**規則**（給模型讀的自然語言指引，模型可能不照做）。要防住一個危險動作，寫在 `CLAUDE.md` 裡求模型別做是不夠的，要用 `permissions.deny`。
</Tip>

## 2. settings.local.json：不進版控的個人覆寫層

`settings.local.json` 與 `settings.json` 同放在 `.claude/` 目錄下，結構完全相同，差別只有一個：**它不該進版控**。官方把它定位為 gitignored 的 local scope \[1]。

典型用途是「只在你這台機器有效、且不該分享給團隊」的設定：

* 指向本機密鑰路徑或個人帳號的環境變數。
* 你個人想加、但不強加給團隊的額外 deny 規則。
* 只在你機器上有意義的 hooks（例如呼叫你本機才有的工具）。

分工判準很簡單：**會想讓團隊每個人都套用的，寫 `settings.json`（進版控）；只屬於你這台機器的，寫 `settings.local.json`（不進版控）。**

<Warning>
  **別讓 settings.local.json 漏進 git**

  Claude Code 首次建立 `settings.local.json` 時通常會把它加進專案 `.gitignore`，但你不該假設它一定生效。動手做第 2 題給你一條 `git ls-files` 的驗證指令。一旦這個檔案連同裡面的密鑰路徑被 commit、再 push，資訊就外洩了，而 git 歷史很難乾淨地抹除。
</Warning>

## 3. 合併優先序：四層加企業 managed 層

同一個設定出現在多個 scope 時，Claude Code 依優先序套用。官方的優先序如下（由高到低）\[1]：

<Steps>
  <Step title="Managed（企業層，最高）">
    不可被任何其他層覆寫，連 command line 參數都壓不過。
  </Step>

  <Step title="Command line arguments">
    當次 session 的臨時覆寫。
  </Step>

  <Step title="Local（.claude/settings.local.json）">
    覆寫 project 與 user。
  </Step>

  <Step title="Project（.claude/settings.json）">
    覆寫 user。
  </Step>

  <Step title="User（~/.claude/settings.json，最低）">
    其他層都沒指定時才生效。
  </Step>
</Steps>

各 scope 的檔案位置（截至 2026-05）\[1]：

| Scope   | 位置                                                                                                                                         |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Managed | macOS `/Library/Application Support/ClaudeCode/`、Linux/WSL `/etc/claude-code/`、Windows `C:\Program Files\ClaudeCode\`（或經 plist / registry） |
| User    | `~/.claude/settings.json`                                                                                                                  |
| Project | `.claude/settings.json`                                                                                                                    |
| Local   | `.claude/settings.local.json`（gitignored）                                                                                                  |

<Warning>
  **permissions 是合併，不是覆寫**

  大多數設定遵循「後讀層覆寫前讀層」的 override 語意，但 `permissions` 是例外：權限規則**跨 scope 合併（merge）**，不是替換 \[1]（這是 [02-1](/code-agent/configuration/config-layer-model) 講的「合併 vs 覆寫」的具體案例）。更關鍵的是 deny 的不可逆性：**任一層 deny 了某個動作，其他任何層都無法 allow 回來**；deny 規則跨所有 scope 先於 allow 評估，所以 user 層的 deny 會擋掉 project 層的 allow \[2]。這修正一個常見誤解：以為 local 或 project 層的 allow 能解開上層的 deny，做不到。要解開只能移除那條 deny。
</Warning>

## 4. permissions 實戰：規則語法與基礎 deny list

權限規則的格式是 `Tool` 或 `Tool(specifier)`，評估順序是 **deny → ask → allow，第一條命中的勝出**，所以 deny 永遠優先 \[2]。一個細節值得記：裸工具名（如 `Bash`）當 deny 規則會把整個工具從模型的 context 移除，模型根本看不到；帶 specifier 的（如 `Bash(rm *)`）則保留工具、只擋匹配的呼叫 \[2]。

各工具的 specifier 語法（截至 2026-05）\[2]：

* **Bash**：`Bash(npm run build)` 精確匹配；`Bash(npm run *)` 前綴匹配；`*` 可出現在任意位置（`Bash(* install)` 匹配任何以 ` install` 結尾的）。注意空格決定詞界：`Bash(ls *)` 匹配 `ls -la` 但不匹配 `lsof`，而 `Bash(ls*)` 兩者都匹配。`:*` 後綴等同尾部 ` *`。複合指令（`&&`、`||`、`;`、`|` 分隔）的每一段都必須各自被規則匹配才放行。
* **Read / Edit**：走 gitignore 規範，四種 path 錨點要分清楚：

| Pattern           | 意義                | 範例                               |
| ----------------- | ----------------- | -------------------------------- |
| `//path`          | 檔案系統絕對路徑          | `Read(//Users/alice/secrets/**)` |
| `~/path`          | home 目錄           | `Read(~/.ssh/**)`                |
| `/path`           | 相對**專案根**（不是絕對路徑） | `Edit(/src/**/*.ts)`             |
| `path` 或 `./path` | 相對當前目錄            | `Read(.env)`                     |

最容易出錯的地方：`/Users/alice/file` **不是**絕對路徑，它相對專案根；絕對路徑要寫 `//Users/alice/file`（兩條斜線）\[2]。bare 檔名走 gitignore 語意，`Read(.env)` 等同 `Read(**/.env)`，匹配任意深度的 `.env`。

* **WebFetch**：`WebFetch(domain:example.com)`。
* **MCP**：`mcp__server`（整個 server）、`mcp__server__*`、`mcp__server__tool`（單一工具）。
* **Agent**：`Agent(Explore)` 控制可用的子代理。

<Note>
  **一份可貼用的基礎 deny list（Windows 友善）**

  你在 Windows + PowerShell 環境，路徑與 shell 有兩個專屬細節：Claude Code 把路徑正規化為 POSIX 形式（`C:\Users\alice` 變 `/c/Users/alice`），所以跨磁碟匹配 `.env` 要寫 `//**/.env`；PowerShell 規則用 `PowerShell(...)` 前綴，形狀與 Bash 相同 \[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 *)"
      ]
    }
  }
  ```

  留意這份只擋讀檔與危險指令，把不可逆的 `git push` 設為 `ask`，其餘走預設。這正是 [03-3](/code-agent/judgment/security-privacy-supply-chain) 最小授權原則的具體落地。
</Note>

關於用權限規則限制網路：官方明確警告 `Bash(curl http://github.com/ *)` 這種想用參數約束 curl 的寫法很脆弱（換 protocol、加 redirect、用變數都能繞過）。更可靠的做法是 deny 掉 `curl` / `wget` 等 Bash 網路工具，改用 `WebFetch(domain:...)` 白名單；或用 `PreToolUse` hook 驗證 URL；真正的 OS 層阻擋則要靠 sandbox \[2]。permissions 與 sandbox 是互補兩層：permissions 擋「Claude 嘗試存取」，sandbox 在 OS 層擋「即使 prompt injection 繞過了 Claude 的判斷」（見 [01-6](/code-agent/foundations/harness-engineering)）。

### 互動式規則評估器

下面的評估器讓你直接試：加規則、輸入工具呼叫，看 deny → ask → allow 優先序怎麼裁定。命中的規則會高亮顯示。

<PermissionEval lang="zh" />

## 5. permission 模式：六種授權階梯

第 4 節的規則是「逐條」授權，permission 模式則是「整體基調」：它設定每個工具呼叫預設要不要停下來問你。模式定 baseline，第 4 節的 allow / ask / deny 規則疊在上面微調。Claude Code 截至 2026-06 有六種模式，依「不問就放行的範圍」由緊到鬆排成一條階梯 \[5]：

| 模式                  | 不問就放行                                                                      | 適用場景                     |
| ------------------- | -------------------------------------------------------------------------- | ------------------------ |
| `default`           | 只有讀取                                                                       | 起步、敏感工作，逐個動作審            |
| `acceptEdits`       | 讀取＋檔案編輯＋常見檔案系統指令（`mkdir` / `touch` / `rm` / `rmdir` / `mv` / `cp` / `sed`） | 你會事後用 `git diff` 審的程式碼迭代 |
| `plan`              | 只有讀取（只研究、提計畫、不改檔）                                                          | 動手前先摸清 codebase          |
| `auto`              | 幾乎全部，但每個動作先過背景安全分類器                                                        | 長任務、減少 prompt 疲勞         |
| `dontAsk`           | 只有預先 allow 的工具與唯讀 Bash 指令（其餘該問的一律拒）                                        | 鎖死的 CI 與腳本               |
| `bypassPermissions` | 全部，連安全檢查都關                                                                 | 只在隔離容器 / VM              |

三個不變量先記住，它們在所有模式都成立 \[5]：

* **deny 與顯式 ask 規則在每個模式都生效**，連 `bypassPermissions` 也擋得住（allow 規則在 bypass 則無意義，因為一切已放行）。要硬擋一個動作，寫 deny，不要靠選模式。
* **protected paths 寫入除了 `bypassPermissions` 外永不自動放行**。`.git`、`.claude`（除 `.claude/worktrees`）、`.vscode`、`.idea`、`.npmrc`、`.mcp.json`、`.claude.json`、各種 shell rc（`.bashrc` / `.zshrc` 等）這類路徑即使你下了 `acceptEdits`、或在 settings 設了 `Edit(.claude/**)` 的 allow，也照樣攔（完整清單見官方 protected paths 節 \[5]）；這道安全檢查跑在 allow 規則之前 \[5]。
* **模式只是 baseline**，第 4 節的規則永遠疊在上面，不是二選一。

### 怎麼切換

三條路 \[5]：

* **session 內**：按 `Shift+Tab` 循環 `default → acceptEdits → plan`。`auto` / `bypassPermissions` / `dontAsk` 不在預設循環裡，要另外開（`auto` 帳號達標才出現、`dontAsk` 永不進循環只能用旗標啟動）。
* **啟動時**：`claude --permission-mode plan`（或其他模式名）。
* **設成預設**：`settings.json` 的 `permissions.defaultMode`。

```json theme={null}
{
  "permissions": {
    "defaultMode": "acceptEdits"
  }
}
```

### 三個模式的雷區

`auto`、`dontAsk`、`bypassPermissions` 各有一個容易誤判的點，分開講。

**`auto`（需 v2.1.83+）不是「自動 yes」，是「分類器擋高風險、其餘放行」**。一個獨立的分類器模型在每個動作執行前審，擋掉超出你請求範圍、打未知基礎設施、或疑似被惡意內容操縱的動作；`curl | bash`、production 部署、force push 到 `main`、對既有檔案的不可逆刪除都預設擋下 \[5]。兩個你必須知道的行為：你在對話裡講的邊界（「先別 push」「等我看過再部署」）會被分類器當成 block 訊號，但邊界只存在 transcript 裡，**context 壓縮掉那句話邊界就失效**，要硬保證得改用 deny 規則。另外 `defaultMode: "auto"` 寫在專案層或 local 層會被忽略（v2.1.142+），免得一個 repo 自己給自己開 auto，要設只能放 `~/.claude/settings.json` \[5]。

**`dontAsk` 是「全自動拒絕」，不是「全自動放行」**。名字容易讀反：它把所有原本該問的呼叫一律拒掉，只有命中你 `permissions.allow` 的工具與唯讀 Bash 指令能跑，連 `ask` 規則都直接拒而非詢問 \[5]。它是給「你已經精確定義好 Claude 能做什麼」的 CI 用的，預先沒 allow 的事一律不做。

**`bypassPermissions` 把安全檢查也關了，只該在隔離環境用**。它連 protected paths 寫入都放行（v2.1.126 起），等於拿掉所有兜底；只有顯式 ask 規則與 `rm -rf /` / `rm -rf ~` 這種對檔案系統根 / home 的刪除仍會擋（最後的 circuit breaker）\[5]。Linux / macOS 上以 root 或 sudo 跑會直接拒絕啟動。官方對它的定位很明確：它對 prompt injection 零防護，要「少問又有兜底」應該用 `auto` 而非 `bypass` \[5]。`--dangerously-skip-permissions` 就是它，名字已經告訴你該多小心。

<Note>
  **同一個任務，選哪個模式**

  你要 Claude 把一個模組重構成多檔，過程會大量改檔，但你想保留審查權：

  * 選 `default`：每次寫檔都停下來問，安全但你會被打斷上百次。
  * 選 `acceptEdits`：改檔不問，跑完你一次 `git diff` 審完；working directory 外的寫入、protected paths、其他 Bash 指令仍會問。**多數「我會事後審」的情境選這個**。
  * 選 `bypassPermissions`：連 protected paths 都不擋。除非你在丟棄式容器裡，否則這是拿可重現性與安全換省事，代價不對等。

  判準：願意事後審 diff（不需逐步）就 `acceptEdits`；要逐步審就 `default`；要長時間無人值守又想保留兜底就 `auto`（且在隔離或低風險倉庫）；`bypass` 只在「即使被 injection 接管也炸不到我」的環境。這正是 [03-3](/code-agent/judgment/security-privacy-supply-chain) 最小授權原則在模式層的落地。
</Note>

<Warning>
  **管理層可以鎖死高風險模式**

  企業 managed settings 能用 `permissions.disableBypassPermissionsMode: "disable"` 鎖掉 bypass、`permissions.disableAutoMode: "disable"` 鎖掉 auto \[5]。團隊環境若你發現某模式切不過去，先看是不是被 managed 層擋了，它壓得過 CLI 旗標。
</Warning>

## 6. 與 claude.json 的分工

一句話分清：`settings.json` 管**策略**（你編輯的行為設定），`claude.json` 管**狀態**（Claude Code 自己維護的工具內部狀態，例如各專案的歷史與信任記錄）。你不該手動編輯 `claude.json`，它不是給人寫的。它的正確路徑、內容格式與該不該動它，見 [02-4](/code-agent/configuration/claude-json)。

## 工具對照

「專案級的行為設定檔」這個概念各家都有，但成熟度與粒度差異大（截至 2026-05，精確格式以各官方文件為準）：

| 概念                   | Anthropic Claude（主範本）                            | OpenAI (Codex)                                                 | Google (Gemini CLI)                   | GitHub Copilot                             | Cursor                         |
| -------------------- | ------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------- | ------------------------------------------ | ------------------------------ |
| 專案級行為設定檔             | `.claude/settings.json` \[1]                     | `.codex/config.toml`（專案根往下、closest wins）＋專案指令 `AGENTS.md` \[3] | `.gemini/settings.json` \[4]          | `.github/copilot-instructions.md`（偏指令，非權限） | `.cursor/rules/*.mdc` + GUI 設定 |
| 個人覆寫（不進版控）           | `.claude/settings.local.json` \[1]               | 以官方文件為準                                                        | 以官方文件為準                               | 以官方文件為準                                    | 以官方文件為準                        |
| 使用者全域設定檔             | `~/.claude/settings.json` \[1]                   | `~/.codex/config.toml` \[3]                                    | `~/.gemini/settings.json` \[4]        | VS Code user settings                      | User Rules（GUI）                |
| 工具權限控制（allow / deny） | `permissions.allow` / `deny` / `ask` 陣列 \[2]     | config.toml 的 `approval_policy` / `sandbox_mode` \[3]          | settings.json 含 tool permissions \[4] | 無對等的細粒度工具權限設定                              | 以官方文件為準                        |
| hooks 掛載點            | `hooks.PreToolUse` / `PostToolUse` / `Stop` \[1] | 以官方文件為準（無直接對應）                                                 | 以官方文件為準                               | 無直接對應                                      | 無直接對應                          |

<Note>
  **對照表只給座標，不給細節**

  各 cell 的精確機制與路徑屬快變動事實，以各官方文件為準。細粒度、machine-enforced 的工具權限（而非自然語言指令）目前以 Claude Code 的 `permissions` 與 Codex 的 sandbox / approval policy 最成熟；Copilot 與 Cursor 的專案級檔案偏「指令 / 規則」性質，靠模型遵循，不是強制邊界。
</Note>

## 動手做

<Steps>
  <Step title="加基礎 deny list 並驗證它真的生效">
    把上面那份基礎 deny list 加進你專案的 `.claude/settings.json`，然後做兩件事確認它生效，而不是只是寫了：

    執行 `/permissions`（Claude Code 內），這個 UI 會列出**合併後**的所有規則以及每條來自哪個 settings 檔，你能直接看到 deny 規則有沒有進到最終策略 \[2]。

    實際試探：要求 Claude 讀取一個被 deny 的路徑（例如 `~/.ssh/` 下的檔案），確認它被攔下而不是讀出內容。記得驗證的是**合併後**的最終策略，不是單一層的設定檔。
  </Step>

  <Step title="建 settings.local.json 放個人變數並確認不進版控">
    建立 `.claude/settings.local.json`，放一個只屬於你機器的 `env` 變數，然後驗證它被 gitignore：

    ```powershell theme={null}
    git ls-files .claude/settings.local.json   # 應回傳空白（代表未被追蹤）
    ```

    若回傳了檔名，代表它已被 git 追蹤，要立刻 `git rm --cached .claude/settings.local.json` 並把對應模式加進 `.gitignore`。
  </Step>
</Steps>

## 常見誤區

<Warning>
  **反模式清單**

  * **以為 local 或 project 的 allow 能解開上層的 deny**：解不開。任一層 deny 即擋，deny 跨所有 scope 先於 allow 評估 \[2]。要放行只能移除那條 deny，不是在更高優先層加 allow。
  * **把 deny list 當唯一安全防線**：deny 規則只作用於 Claude 的內建檔案工具與它識別得出的 Bash 檔案指令（`cat`、`head` 等），擋不住一個 Python / Node 腳本自己開檔讀取。要 OS 層阻擋所有 process，得開 sandbox \[2]。
  * **在 env 明文存放密鑰**：`env` 會注入每個 session，把 API key 明文放進去等於把它留在設定檔裡。改用 `apiKeyHelper` / `awsCredentialExport` 這類 helper script 動態產生，或指向 secret manager，不要明文。
  * **手動編輯 claude.json**：那是 Claude Code 自己維護的狀態檔，不是策略檔（見 [02-4](/code-agent/configuration/claude-json)）。要改行為，改 `settings.json`。
  * **把 settings.local.json 當成「優先序最高」**：它確實覆寫 project 與 user，但壓不過 managed 企業層與 deny 規則，而且它和 `settings.json` 是**疊加**關係，不是取代。
  * **把選模式當成擋危險動作的手段**：模式只設 baseline，deny 才是硬牆，且在每個模式（含 `bypassPermissions`）都生效。`bypassPermissions` 連 protected paths 都放行，靠它省事等於拿掉兜底；要少問又安全用 `auto`（第 5 節）。
</Warning>

## 自我檢核

<Check>
  **通過本單元的標準**

  1. 你能不能用一句話說清楚 `settings.json`（策略，machine-enforced）與 `CLAUDE.md`（規則，模型可能不照做）的分工？要擋一個危險動作，你會寫在哪個檔？
  2. 你的專案 `.gitignore` 是否已排除 `settings.local.json`？`git ls-files .claude/settings.local.json` 回傳的是空白嗎？
  3. 給你一條 `Read(/Users/alice/secret)` 規則，它擋的是絕對路徑還是專案根下的路徑？要擋絕對路徑該怎麼寫？
  4. 你現在生效的 deny 規則，是看單一設定檔得知，還是用 `/permissions` 看合併後的最終策略確認的？
  5. user 層寫了 `allow`、project 層寫了同一動作的 `deny`，最後這個動作會被允許還是擋下？
  6. `dontAsk` 模式把該問的動作自動放行還是自動拒絕？`bypassPermissions` 與 `auto` 的根本差別在哪？
</Check>

## 來源與延伸閱讀

事實主張依官方文件，快變動項標註截至 2026-05（permission 模式一節截至 2026-06）。

<div className="references">
  * \[1] Anthropic, "Claude Code settings," Claude Code Docs. （settings.json 頂層鍵 reference；四層加 managed 的優先序 Managed → CLI args → Local → Project → User；permissions 跨 scope 合併而非覆寫；model 鍵接受別名與完整 ID） [https://code.claude.com/docs/en/settings](https://code.claude.com/docs/en/settings) （截至 2026-05）

  * \[2] Anthropic, "Configure permissions," Claude Code Docs. （規則語法 `Tool(specifier)`；評估序 deny → ask → allow first-match-wins；任一層 deny 不可被其他層 allow；Read / Edit 的 gitignore 四種 path 錨點與 Windows POSIX 正規化；權限由 Claude Code 強制而非模型；`/permissions` 檢視合併規則；permissions 與 sandbox 互補） [https://code.claude.com/docs/en/permissions](https://code.claude.com/docs/en/permissions) （截至 2026-05）

  * \[3] OpenAI, "Codex configuration," OpenAI Developers Docs. （user 全域 `~/.codex/config.toml` 與專案 `.codex/config.toml`（專案根往下、closest wins）；`approval_policy` / `sandbox_mode`；`AGENTS.md` 為 Codex 的專案指令、等價於 CLAUDE.md） [https://developers.openai.com/codex/config-reference](https://developers.openai.com/codex/config-reference) （截至 2026-05）

  * \[4] Google, "Gemini CLI configuration," Gemini CLI Docs. （user `~/.gemini/settings.json` 與專案 `.gemini/settings.json`，project 覆寫 user；settings.json 控制 tool permissions、MCP server、workspace 等） [https://geminicli.com/docs/reference/configuration/](https://geminicli.com/docs/reference/configuration/) （截至 2026-05）

  * \[5] Anthropic, "Choose a permission mode," Claude Code Docs. （六模式 default / acceptEdits / plan / auto / dontAsk / bypassPermissions 的放行範圍；`Shift+Tab` 循環與 `--permission-mode` 旗標、`permissions.defaultMode`；deny 與顯式 ask 在所有模式生效；protected paths 除 bypass 外不自動放行；auto 背景分類器、需 v2.1.83+、對話邊界隨 context 壓縮失效、`defaultMode: auto` 於專案／local 層被忽略；dontAsk 自動拒絕；bypassPermissions 關安全檢查、root／sudo 拒啟、`disableBypassPermissionsMode`／`disableAutoMode` managed 鎖） [https://code.claude.com/docs/en/permission-modes](https://code.claude.com/docs/en/permission-modes) （截至 2026-06）
</div>

* 銜接：[02-1](/code-agent/configuration/config-layer-model) 設定層級與「合併 vs 覆寫」的通用模型、[02-4](/code-agent/configuration/claude-json) claude.json 狀態檔的定位、[01-6](/code-agent/foundations/harness-engineering) 權限邊界在 harness 設計中的位置、[03-3](/code-agent/judgment/security-privacy-supply-chain) 最小授權與供應鏈風險、[04-6](/code-agent/customization/hooks) 用 hook 擴充權限評估與確定性把關。
