五個機制解五種不同問題。第一個判斷永遠是:這條規則能不能容忍被違反?
這條 guidance…
│
├─ 絕不能被違反(compliance / 金流 / 破壞性操作)?
│ ├─ 是「擋某個工具呼叫」 → settings permissions(deny/ask 規則)
│ └─ 要「檢查內容再決定/轉換」 → hooks(PreToolUse 擋、PostToolUse 轉換)
│
└─ 是建議/慣例(違反了頂多品質差)?
├─ 全專案、每個 session 都該知道 → CLAUDE.md
├─ 只有某類檔案適用 → .claude/rules/ + paths glob(省 context)
└─ 多步驟流程、偶爾才用 → Skill(被呼叫才載入)
| 機制 | 給什麼 | 何時載入 | 強制力 |
|---|---|---|---|
| CLAUDE.md | 事實性/持續性規則(build 指令、慣例、架構) | 每 session 開始全載入(沿目錄樹往上) | ❌ context,「盡量遵守」 |
| .claude/rules/ + paths | 只適用部分檔案的規則 | 有 paths → 碰到 match 檔案才載入 | ❌ 同上 |
| Skills | 多步驟程序/工作流 | 被呼叫才載入 body | 部分(allowed-tools 預核工具) |
| Hooks | 「不論 Claude 怎麼決定」都要執行/擋下 | 事件觸發跑 callback | ✅ 真強制 |
| settings permissions | 工具/檔案/網域存取控制 | 每次工具呼叫前評估 | ✅ 真強制,deny 最大 |
@path import——相對路徑以「含 import 的那個檔」為基準、最大遞迴 4 層、code span 內的 @ 不解析。官方建議 CLAUDE.md 保持精簡(巨檔 → 拆進 .claude/rules/ 主題檔)。題:migration SQL 檔散落各 service 的 migrations/,要統一慣例、編輯到就自動套用、context 成本最低。
推理:①「自動套用」→ 刪掉 Skill(要人叫);②「慣例」不是 compliance → 不用 hook;③「只有某類檔案」→ root CLAUDE.md 常駐太貴、per-dir CLAUDE.md 要同步 12 份;→ .claude/rules/ + paths: ["**/migrations/*.sql"],match 才載入。
但若題目改一個字:「migration 檔絕不允許出現 DROP TABLE」→ 這是 must-never → PreToolUse hook 檢查 Edit/Write 內容,rules 檔擋不住。
npm run clean 或 python x.py 裡面做了什麼。OS 層還有第六層 sandbox 補這個洞 → 見 1.5 末的延伸框。| Mode | 不問就能做 | 典型場景 |
|---|---|---|
default(別名 manual) | 只有讀 | 一般互動 |
acceptEdits | 讀 + 檔案編輯 + 常見檔案系統指令(mkdir/touch/rm/mv/cp/sed),限 working dir / additionalDirectories 內 | 信任的編輯流程 |
plan | 只有讀(唯讀探索) | 改動前探索、出計畫等核准 |
auto | 全部,但有背景 classifier 安全檢查 | 需新模型;Team/Enterprise 由 Owner 開啟(個人 plan 不用) |
dontAsk | 只有 allow 清單內 + 內建唯讀指令;其餘直接 deny、不會問。例外:AskUserQuestion、組織設為 ask 的 connector 工具、標了 requiresUserInteraction 的 MCP 工具——放進 allow 也照樣被 deny | CI / 腳本(沒有人能回 prompt) |
bypassPermissions | 全部(含 protected paths);但 deny 規則仍然生效。仍會 prompt 的:explicit ask 規則、組織設為 ask 的 connector 工具、requiresUserInteraction 的 MCP 工具、rm -rf circuit breaker | 只限隔離容器 / VM |
.git、.claude、.bashrc、.npmrc…)除 bypassPermissions 外都不會被自動核准——default/acceptEdits/plan 照樣 prompt、auto 進 classifier、dontAsk 直接 deny、allow 規則也蓋不掉;唯 bypassPermissions 直接放行 protected paths(見上表),這正是它只限隔離環境的原因;② rm -rf /、rm -rf ~ 在所有 mode 含 bypass 都有 circuit-breaker prompt;③ --dangerously-skip-permissions = bypassPermissions 的旗標形式;④ bypassPermissions ≠ 無設防——settings 的 deny 規則在 bypass 下照樣擋(✓ 實測:deny Bash(rm *) + --permission-mode bypassPermissions → rm 仍被拒)。「bypass = 全部放行、什麼都擋不住」是選項裡的假敘述。dontAsk(不在 allow 清單 → 自動拒絕、流程繼續可控),不是 bypassPermissions(全放行 = 把 CI runner 的一切交給 agent,官方限定隔離環境)。兩者都「不會卡住」,差在預設方向:一個 deny、一個 allow。優先序(高 → 低) permission rules 例外 Managed(企業,誰都蓋不掉) ├ 不是覆蓋,是 merge ↓ CLI 參數 ├ 任一層的 deny 永遠贏 ↓ .claude/settings.local.json(個人)├ 高層 allow 蓋不掉低層 deny ↓ .claude/settings.json(team 共用) └ 評估順序固定: ↓ ~/.claude/settings.json(user) deny → ask → allow(先中先贏)
Bash(git push *) + allow Bash(git push origin dev) → 照樣 deny(deny 先評估)。Bash)= 整個工具從 context 移除;Bash(rm *) = 保留工具、擋特定呼叫。.mcp.json 進 repo team 共用|~/.claude.json 個人;secrets 走 ${ENV_VAR} 展開絕不進 commit;server 連線時 tools 一次全部 discover。先記這句反直覺的:權限規則裡的 /Users/alice/file 不是絕對路徑。
單斜線開頭的路徑是半截的——Claude Code 會自動幫你補上前面那一段。補什麼,看這條規則寫在哪個檔案。所以同一行字,換個檔案放就管到不同地方:
同一行規則:Edit(/src/**) 寫在專案的 .claude/settings.json 自動補上 /Users/you/myapp 你寫的 /src/** 實際管到 /Users/you/myapp/src/** ✅ 正是你要的 寫在 ~/.claude/settings.json 自動補上 /Users/you/.claude 你寫的 /src/** 實際管到 /Users/you/.claude/src/** ❌ 這資料夾根本不存在
/Users/alice/file isn't an absolute path. The single leading slash anchors at the settings source, not the filesystem root."四種寫法(斜線越多,起點越靠前) src/** → 目前工作目錄 /src/** → 半截路徑,自動補前綴(補什麼看寫在哪個檔) //src/** → 完整路徑,放哪個檔都等於 /src/** ~/src/** → 家目錄底下
// 或 ~/,永遠不用單斜線。.claude/settings.json 會進 git 給十個人 clone,同一行 Edit(/src/**) 要各自對到他自己那份 src/。寫死絕對路徑就只有你一台電腦能用。這就是「自動補前綴」這個機制存在的理由。.claude/settings.json(project) → <專案根>/path .claude/settings.local.json → <啟動 Claude 的 cwd>/path ~/.claude/settings.json(user) → ~/.claude/path ← 最容易中招 --settings <file> → <該檔所在目錄>/path CLI 旗標 / /permissions → <啟動 cwd>/path
user settings 那條為什麼特別反直覺:專案設定會往上跳一層、跳出 .claude/(檔案放在 <專案根>/.claude/,補的前綴卻是 <專案根>);user settings 不跳,就停在 ~/.claude。你以為前綴是家目錄,其實中間多一段 .claude。
官方自己舉的例子:user settings 裡的 Read(/secrets/**) 擋的是 ~/.claude/secrets/**,不是你專案裡那個 secrets 資料夾。
Read(//<abs>/**) → 「denied by your permission settings」;Read(/<abs>/**) → 讀得到,規則沒匹配。Edit(<path>) 的 deny 也會擋住 Bash 的 rm <path>(拿掉規則就刪得掉,反證確認)。官方只列了 cat/head/tail/sed,沒承諾 rm——所以正式配置仍要另外寫 Bash(rm *),不要依賴未文件化的行為。| 旗標 | 作用 | 考點鑰匙 |
|---|---|---|
-p / --print | 非互動,印完就退 | 「CI 卡住等輸入」 |
--output-format | text / json / stream-json(只有這三個值) | 選項出現 markdown/xml = 假值 |
--json-schema | 最終輸出強制符合 schema | 「下游機器讀」和 json 搭配 |
--max-turns | 回合上限,預設無上限,到頂 exit error | 「防 runaway / 爆預算」成對出現 |
--max-budget-usd | 美金成本上限,到頂即停 | |
--permission-mode | 六 mode 擇一 | CI 用 dontAsk |
--allowedTools / --disallowedTools | 工具 pattern 清單 | review agent 限唯讀 |
--setting-sources | 載入哪幾層 settings(user,project,local) | 「不信任 fork 的 project settings」 |
--mcp-config + --strict-mcp-config | 指定且只用該 MCP 設定 | CI 環境鎖定 server 來源 |
claude -p "Review yesterday's merged changes" \ --output-format json --json-schema ./findings-schema.json \ --permission-mode dontAsk \ --allowedTools "Read" "Grep" "Glob" "Bash(git log *)" "Bash(git diff *)" \ --max-turns 30 --max-budget-usd 5.00
每個旗標對一個考點:非互動(-p)、機器可讀(json+schema)、無人回 prompt(dontAsk)、least privilege(唯讀工具)、防 runaway(兩個上限)。考題常抽掉其中一個問「還缺什麼」。
user prompt ──▶ [UserPromptSubmit]* ──▶ Claude 決定呼叫工具
│
[PreToolUse]* ─┤ exit 2 = 擋下(工具不執行)
▼
工具執行
│
[PostToolUse] ─┤ 只能轉換結果/補 context
▼ (動作已發生,擋不了)
結果進 agent context
Claude 想結束 ──▶ [Stop / SubagentStop]* ── exit 2 = 不准停、繼續做
context 快滿 ──▶ [PreCompact]* (* = 可 block 的事件)
0 成功|2 blocking(只對 pre-action 事件有效)|其他非零 = non-blocking error。hooks 欄位(各層級皆可);Skill/Agent frontmatter 也能掛 scoped hooks。不在 Module 1 考綱,備考不用背;但不知道它,實務判斷會出錯,所以放這裡。
前五個機制(含 hooks)都在指令送出去之前判斷,手上的材料只有指令字串。所以下面這三行對它們全部無害:
npm run clean # package.json 裡是 "clean": "rimraf dist"
make clean # Makefile 裡是 rm -rf build/
python cleanup.py # 第 40 行 os.remove("Atlas/Dots/x.md")
字串裡沒有 rm、沒有目標路徑 → deny 規則和 hook 全部放行,而且它們沒出錯,只是看不到。Sandbox 攔在另一個位置:指令已經在跑,作業系統在它真的去碰檔案的那一刻拒絕(macOS Seatbelt/Linux 與 WSL2 用 bubblewrap)。限制掛在行程上、而且子行程繼承,所以 python/npm/make 叫出來的一切都在同一圈裡。
階段一:Claude Code 決定要不要送出這個指令 ← deny 規則 / hook(看字串,懂語意) 階段二:OS 執行這個行程 ← sandbox(看實際碰的檔案,不懂語意)
| 看得懂 | 看不懂 | |
|---|---|---|
| 階段一 | 語意:這是 push、這是寄信、這是刪檔 | 包裝過的(bash deploy.sh 內部) |
| 階段二 | 事實:這個行程正在寫這個路徑 | 意義:這條連線是查天氣還是寄信 |
兩邊的漏洞不重疊,所以是疊加不是取代。只留階段二擋不住 git push --force——它不寫任何受保護檔案,OS 只看到一條到允許網域的連線。而且階段二沒有「跳出來問你」這個選項:指令跑到一半,OS 只能允許或 Operation not permitted。
Read/Edit/Write 不開子行程,走的是權限系統,不在 sandbox 裡。所以兩層必須一起設;好消息是 sandbox.filesystem 的路徑會和 Read/Edit 的 deny 規則合併成同一個邊界。settings.json 的 sandbox 欄位(或跑 /sandbox,會寫進 .claude/settings.local.json)。陣列 key(allowWrite/denyWrite/excludedCommands)跨層合併,布林 key(enabled)由高層決定。docker 不相容、jest 要加 --no-watchman、macOS 上 Go 寫的 CLI(gh/gcloud/terraform)TLS 會失敗——都得進 excludedCommands。原生 Windows 不支援(要用 WSL2)。--allowedTools 只給 Read/Grep/Glob(+ 唯讀 git)。不給 Edit/Write = review agent 不可能改壞東西。--output-format json + --json-schema,讓 bot 能 parse 成 PR comments / gate 條件。| 名詞 | 是什麼 | 不講就會被誤報成 |
|---|---|---|
| barrel file | 只做轉出的 index.ts:export * from './a'; export * from './b',讓別人寫 import { a, b } from './utils' 就好 | 「多餘的間接層/增加 bundle 體積/循環 import 風險」——但它其實是你刻意的模組對外邊界 |
| re-export | 上面那個動作本身:把別的檔案的東西原封不動再轉出一次 | 「沒必要的包裝」 |
| generated code | 工具產生的程式碼:OpenAPI client、Prisma types、protobuf | 一堆風格與型別 finding——但你改不了,下次重生就蓋掉 |
| vendored 依賴 | 把第三方套件原始碼直接複製進 repo(vendor/、third_party/) | 審別人的專案,對你零價值 |
寫的時候要帶「為什麼」:只寫「barrel files are accepted」不夠——要寫「barrel files are our module public API boundary, do not flag as unnecessary indirection」,模型才分得出怎樣算違規、怎樣算正常。
真正的代價不是吵:一次 PR 冒出 200 條 finding、其中 180 條來自 generated code,人看到第三頁就放棄——從此不再看 review 結果。漏一條真 finding 傷害一次;讓人放棄看 review,傷害的是往後每一次。
assert getter 回傳值)。沒有準則,模型會大量生成後者衝覆蓋率。getter = 只負責把存進去的值原樣拿出來的方法。assertion = 測試裡那行 expect(...) / assert ...。
trivial assertion = 你斷言的那件事,中間沒有經過任何邏輯。
// trivial —— 存 Alice、取出 Alice,中間什麼都沒發生
it('returns the name', () => {
expect(new User('Alice').getName()).toBe('Alice')
})
// behavioral —— 每一條對應一個真的會發生的災難
it('折扣不會讓價格變負數', () =>
expect(calc.apply({ price: 100, discount: 150 })).toBe(0))
it('API timeout 時回傳 cached 值而不是丟例外', async () =>
await expect(client.fetchRates()).resolves.toEqual(CACHED_RATES))
it('空清單不會炸,回傳空結果', () =>
expect(summarize([])).toEqual({ total: 0, items: [] }))
判準一句話:這條測試失敗的時候,是哪個使用者遇到什麼問題?答不出具體場景 → 不要寫。上面第一條只有在 JavaScript 的 return 壞掉時才會失敗,那不是你的 bug;它覆蓋率 +1、抓 bug 機率≈0、欄位改名還要跟著改,淨值是負的。
但 getter 裡有邏輯就該測:get displayName() { return this.nickname ?? `${first} ${last}`.trim() } 有分支(有沒有 nickname)、有邊界(first 是空字串會多一個空格)——這些是真的會寫錯的地方。判準還是同一句:中間有沒有「可能寫錯的判斷」。
fixture = 測試用的預備資料或預備狀態(假使用者、假 API 回應、先建好的 DB 記錄)。「定義 fixture 慣例」= 講清楚這些東西怎麼建、放哪個目錄、跨測試怎麼共用;不講,模型會在每個測試檔各造一套。
寫進 CLAUDE.md 的收尾要明講:「寧可 5 條測到真問題,不要 40 條把覆蓋率衝到 90%;覆蓋率不是本專案的驗收指標。」——不講這句,模型就會自己把覆蓋率當成唯一看得到的分數去追。
Bash(git push *)(user 層)+ allow Bash(git push origin main)(project 層)+ 一個 PreToolUse hook 回 allow——最後 git push origin main 會怎樣?為什麼?dontAsk 和 bypassPermissions 都「不會卡 prompt」,什麼情境下選錯會出大事?--max-turns 預設無上限這個事實在考題裡重要?