TL;DR 與結論
你一直在 Claude Code 裡重打的四項工作,各自屬於不同的檔案。發佈檢查清單是 skill。絕不能破的規則是 PreToolUse hook。要讀很多檔案的稽核是 subagent。Claude 看不到的資料是 MCP server。
我們在一個真實的 repo 上跑了這四項,那是一個有 34 個測試的請款 API,而且每次都先選錯。選錯的結果都在畫面上失敗:
- 放進 CLAUDE.md 的檢查清單,在 /context 裡多了一行
Memory files: 210 tokens,而且每一輪都載入。 - 第一個 hook 執行了、印出警告,寫入卻照樣通過(unitPrice 從 8000 變成 12000)。
- 直接在對話裡跑稽核灌爆了對話串:Messages 是 19.3k token,而指名 subagent 時是 7.2k。
- 沒有文件 server 時,Claude 回答「I couldn't find that setting... I won't guess one.」
結論:從文件的表格選出種類,再讓檔案真的給出回答,而不只是存在。只記錄的 hook 屬於建議性質,就像它取代的那一行 CLAUDE.md。這四種之中,只有 hook 從不詢問。
複製下面的檔案、commit,然後從你重打最多次的那一項開始。
有來源的綜合整理
第一個字之前,你的工作階段載入了什麼。 我們的基準 /context 讀數是 26.2k/1m token,沒有記憶檔那一行,也沒有 MCP 那一行。你加進去的每一項工作,不是在那張表上多一行,就是沒有。先讀它,再拿它當基準去測量。
CLAUDE.md 的規則是請求,hook 是強制。 文件說,CLAUDE.md 或 skill 裡像「never edit .env」這樣的指示是請求,而擋下編輯的 PreToolUse hook 才是強制 s1。Memory 頁面把 CLAUDE.md 稱為 context,而不是強制 s8。Hacker News 上的一個討論串從 hook 這一側回報了同樣的事:以 0 結束的 hook 什麼都擋不下 s10。
觸發表告訴你該用哪個檔案。 同一套程序貼到第三次,就是 skill。輸出灌爆聊天,就是 subagent。Claude 看不到的分頁,就是 MCP server。必須每次都發生的事,就是 hook s1。
工作 1,發佈檢查清單,是 skill。
- 在 CLAUDE.md 裡長大的程序,屬於 skill,而它的內容只在使用時才載入 s3。
- 在我們的執行中,貼進 CLAUDE.md 的檢查清單多了一行
Memory files: 210 tokens。改成設有disable-model-invocation: true的 skill 後,/context 回到了基準線。 /release跑完六個步驟(34 個測試、0.4.0 升到 0.4.1、tag v0.4.1)。它還抓到git commit -am會略過未追蹤的檔案,所以先把新的 changelog 加進暫存區。
工作 2,必須成立的規則,是 hook,而保證在於它回傳的答案。
- 以 0 結束又沒有輸出,就不是決定。以 2 結束會擋下。deny JSON 也會擋下 s2。
- 我們的第一個 hook 只記錄、以 0 結束,寫入照樣通過(unitPrice 從 8000 變成 12000)。Claude 的回覆完全沒提到 hook。
- 同一個 hook 改為回傳 deny JSON 後,編輯被拒絕,錯誤訊息是:
PreToolUse:Edit hook error: Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).接著 Claude 問:要覆寫,還是改寫 migration。 - 文件公開了一份保護檔案用的 PreToolUse 做法 s23。
- 限制:這個 hook 監看的是 Edit 和 Write。也要涵蓋 shell,或是讓 shell 不在你的允許清單上。
onFailure: "block"讓失敗的 hook 擋下動作,而不是放行。它在 v2.1.295 加入 s2 s15。
工作 3,灌爆對話的稽核,是 subagent。
- subagent 從什麼都不知道開始,並交回一份摘要 s4。
- 直接在對話裡跑時,Messages 是 19.3k。用指名的 subagent 時是 7.2k。
- 光有 agent 檔案,2 次中委派 0 次。在提示裡指名它,就委派了。檔案讓它存在,你的提示讓 Claude 去用它。
- 在檔案裡固定
model:。帳單沒有變:閱讀仍然會發生,只是不在你的視窗裡。
工作 4,Claude 碰不到的資料,是 MCP server。
claude mcp add --scope project會寫出.mcp.json,而 tool search 只載入工具名稱 s5。- Claude Code 自己的文件有一個免費、不需登入的 HTTP server,提供
search_claude_code_docs和query_docs_filesystem_claude_code_docss24。 - 沒有它時,Claude 回答「I couldn't find that setting... I won't guess one.」有了它,Claude 回答的是
"onFailure": "block",在 v2.1.295 加入,而我們的安裝是 2.1.294。 - 為模型已認識的函式庫接文件 server,對我們沒有任何改變。要連的是模型不可能擁有的資料。
現場回報。 一個 Reddit 討論串回報說,Claude 會忽略 CLAUDE.md 裡一行「MUST use agent」 s9。我們把它當作回報來引用,而不是測量結果。Anthropic 的工程文章也談到同樣的區分 s6 s7。
程式碼
這次執行的四個檔案,加上兩個輔助檔。路徑是每個檔案在你 repo 裡的位置。
1. 發佈 skill,位置在 .claude/skills/release/SKILL.md。輸入 /release。disable-model-invocation: true 讓它只能由你啟動,這正是會 commit 和打 tag 的工作所需要的。
SKILL.md
---
name: release
description: Cut a patch release of ledgerly (typecheck, tests, version bump, changelog line, commit, tag).
disable-model-invocation: true
---
Run these steps in order and stop at the first failure:
1. `pnpm typecheck` must exit 0.
2. `pnpm test` must pass.
3. Bump the patch version in `package.json` (e.g. 0.4.0 -> 0.4.1).
4. Add one line at the top of `CHANGELOG.md` (create it if missing): `## vX.Y.Z - YYYY-MM-DD` followed by a one-line summary of the commits since the last tag.
5. `git commit -am "release vX.Y.Z"`
6. `git tag vX.Y.Z`. Never push.
2. 保護資料的 hook,位置在 .claude/settings.json。它回傳 deny 決定,並附上 Claude 會讀到的理由。它需要 PATH 裡有 jq,而且可以內嵌,所以這個 hook 不需要自己的腳本檔。請把 data/.*\.json$ 的模式和理由換成你的規則。
settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -c 'if (.tool_input.file_path // \"\") | test(\"data/.*\\\\.json$\") then {hookSpecificOutput: {hookEventName: \"PreToolUse\", permissionDecision: \"deny\", permissionDecisionReason: \"Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).\"}} else empty end'"
}
]
}
]
}
}
3. conventions auditor,位置在 .claude/agents/conventions-auditor.md。工具僅限唯讀,模型是固定的。請依你的 README 修改慣例清單。
conventions-auditor.md
---
name: conventions-auditor
description: Use when asked to audit the code against the README conventions
model: sonnet
tools: Read, Grep, Glob
---
You audit ledgerly's source against README § Conventions:
- Money is always an integer number of cents. Never floats.
- Domain functions return new objects; they never mutate their input.
- Validation failures throw `ValidationError` (400), missing records `NotFoundError` (404), illegal state transitions `ConflictError` (409).
- A change of stored shape bumps `schemaVersion` and adds a migration in `src/store/migrations.ts`.
Read every file under `src/`. Return only the findings: one line per violation as `file:line - convention - what is wrong`, then borderline cases the same way, then one line with the count of files read. No file contents, no summary of clean files.
conventions-auditor.md on GitHub
4. MCP 設定,以 .mcp.json 存放在 repo 根目錄。它就是 claude mcp add --transport http claude-docs --scope project https://code.claude.com/docs/mcp 寫出的內容。Claude Code 會把 project 的 server 列為 pending,直到你在下一個工作階段核准它。
mcp.json
{
"mcpServers": {
"claude-docs": {
"type": "http",
"url": "https://code.claude.com/docs/mcp"
}
}
}
輔助檔。 ctx.sh 在無介面的工作階段裡執行 /context 並印出 token 那幾行,讓你比較前後差異。mcp-probe.sh 會對一個 HTTP MCP 網址送出 initialize 和 tools/list,讓你在加入之前先確認 server 有回應。
ctx.sh
#!/usr/bin/env bash
# Usage: ctx.sh <step> -> runs "/context" through run-session.sh, writes context-<step>.txt (honours STRICT_MCP / EXTRA_ALLOW)
C="$(cd "$(dirname "$0")" && pwd)"
"$C/run-session.sh" "$C/out/context-$1.json" "/context" >/dev/null
python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(d['result'])" "$C/out/context-$1.json" > "$C/context-$1.txt"
grep -E '^\*\*Tokens|^\| (System prompt|System tools|MCP tools|Memory files|Skills|Custom agents|Messages) ' "$C/context-$1.txt"
mcp-probe.sh
#!/usr/bin/env bash
# Usage: mcp-probe.sh <url> [tools/call json params] -> initialize + notifications/initialized + tools/list (+ optional tools/call) over streamable HTTP
U="$1"; H=$(mktemp)
curl -s -D "$H" -m 30 -X POST "$U" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
echo; SID=$(grep -i '^mcp-session-id:' "$H" | tr -d '\r' | cut -d' ' -f2); echo "session-id: ${SID:-none}"
HS=(-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream'); [ -n "$SID" ] && HS+=(-H "Mcp-Session-Id: $SID")
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; echo
[ -n "${2:-}" ] && { curl -s -m 90 -X POST "$U" "${HS[@]}" -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":$2}"; echo; }
rm -f "$H"
週一要做的事
- 開啟一個全新的工作階段並執行 /context。記下總量。那就是你的基準線。
- 選出你貼最多次的檢查清單。把它移到
.claude/skills/<name>/SKILL.md,如果它會 commit 或打 tag,就加上disable-model-invocation: true,再用/<name>執行。 - 把那條絕不能破的規則放進一個 PreToolUse hook,讓它回傳
permissionDecision: "deny"或以 2 結束。要求 Claude 去破壞這條規則,並確認編輯被拒絕。 - 如果你的 hook 只記錄,就改掉它。以 0 結束的 hook 什麼都擋不下。
- 把一項要讀很多檔案的工作,移到
.claude/agents/<name>.md,並固定model:。在提示裡指名它,直到 Claude 自己會選它。 - 檢查最近一件 Claude 幫你猜的事實。如果它比模型還新,就用
claude mcp add --transport http <name> --scope project <url>加入文件 server,並 commit.mcp.json。 - 在依賴某個設定之前,先檢查你的 Claude Code 版本。
onFailure: "block"需要 2.1.295。 - 執行
git add -A && git status,並 commit 新檔案。我們的執行恰好顯示四個。
延伸閱讀
選擇卡。 一頁就夠。這些是文件的列 s1,上面寫著我們的四項工作。這個資源包的主角,就是上面的四個檔案加上這張卡。
| 種類 | 它做什麼 | 何時使用 | 我們的工作 | 我們的檔案 |
|---|---|---|---|---|
| CLAUDE.md | 每次對話都載入的持久 context | Claude 錯了兩次的慣例 | 一行,不是一套程序 | 無 |
| Skill | Claude 可以使用的指示與工作流程 | 同一份操作手冊你貼到第三次 | 發佈檢查清單 | .claude/skills/release/SKILL.md |
| Hook | 由事件觸發的腳本 | 你希望它每次都發生、而且不用詢問 | 絕不手動編輯儲存的資料 | .claude/settings.json |
| Subagent | 隔離的 context,交回摘要後的結果 | 附帶工作灌爆你的對話 | conventions 稽核 | .claude/agents/conventions-auditor.md |
| MCP | 連接外部服務 | 你一直從 Claude 看不到的分頁複製資料 | Claude Code 文件 | .mcp.json |
| Plugin | 把這四種打包成一個可安裝的單元 | 第二個 repository 需要同樣的設定 | 未使用 | 無 |
這張卡決定的是種類,不是品質。 只記錄的 hook、你從不指名的 subagent,以及為模型已認識的文件而接的 server,都屬於對的種類,卻都失敗了。先選出種類,再讓檔案給出回答。
何時不該用這些。
- Claude 一直做錯的慣例,以一行的形式留在 CLAUDE.md,而不是一套程序。
- 需要你來回調整、而且要看著歷史紀錄的工作,留在主對話串,不要放進 subagent。
- 為模型已認識的函式庫接文件 server,只會耗掉 context,什麼都不會改變。
pnpm test是你已經有的 CLI。它不是 MCP 的工作。
Plugin。 plugin 是把這四個一起送出的盒子。當另一個 repo 需要同樣的設定時再用它。
Skill 加 MCP。 文件把它們配成一對:server 提供連線,skill 教 Claude 如何好好使用它。
帶有 context: fork 的 skill。 skill 可以在分叉出來的 subagent 裡執行,讓它的閱讀留在你的視窗之外。在依賴它之前,先拿一項工作測試。
command 以外的 hook 類型。 command 是我們用的那一種。hooks 參考文件列出了其他處理器類型,包括 http 和 prompt hook。規則相同:決定權在 hook 回傳什麼,而沉默的 hook 什麼都沒決定。
MCP 對上 CLI。 關於 MCP server 吃掉 context 的爭論仍在進行,有一篇主張先用 CLI 的文章 s11,另一篇談 MCP server 的 context 成本 s12。我們的規則:如果 CLI 已經能做這件事,就用 CLI。MCP 留給沒有 CLI 的情況。tool search 現在只載入名稱,所以在做決定之前,先在你自己的環境用 /context 測量 s5。
skill 要短,而且要驗證過。 每項工作一個簡短的 skill,並對照一次真實執行來檢查。可以參考 SkillsBench 論文 s13,以及一位實務工作者說明為何 skill 在使用之前成本很低的看法 s14。
FAQ
為什麼我的 hook 執行了,編輯卻還是通過? hook 只憑它的指令回傳什麼來做決定。以 0 結束又沒有輸出,就不是決定 s2。請回傳 deny JSON,或以 2 結束。
我有 subagent,為什麼 Claude 還是自己讀檔? 在我們的執行中,光有 agent 檔案,2 次中委派 0 次。請在提示裡指名它,例如「use the conventions auditor subagent」。
這個 hook 涵蓋 Claude 能做的所有事嗎? 不。我們的只比對 Edit 和 Write。Claude 在之後的一次執行中提議改用 shell 來完成變更,所以請也涵蓋 shell,或讓它不在你的允許清單上。
我該為用到的每個函式庫都加一個 MCP server 嗎? 不用。如果模型已經認識那個函式庫,server 什麼都不會改變,只會耗掉 context。要為比模型還新、或放在磁碟之外的資料才加,並且把你安裝的版本,對照加入該功能的版本來檢查。
我可以把這一切都 commit 嗎?
可以。skill、設定檔、agent 檔案和 .mcp.json 都放在 repo 裡,所以任何 clone 它的人都拿得到。project 的 MCP server 要在下一個工作階段核准一次。
AIDive