你一直重打的四個提示,變成四個檔案
Claude Code 擴充功能,就是你放進 repo 裡的檔案,讓你不必再重打同一段提示:skill、hook、subagent 和 MCP server。我們在一個真實的 repo 上跑了四項重複出現的工作,那是一個用 JSON 檔當資料庫的小型請款 API。每項工作一開始都給了錯的擴充功能,而選錯的結果每次都在畫面上失敗。接著,每項工作都變成一個提交進 repo 的檔案。本頁涵蓋這四個動作,以及決定該用哪一種的選擇卡。
有兩個結果是下面所有內容的背景。hook 常被說成保證,記憶檔裡的規則則被說成請求,但我們的第一個 hook 確實執行了、印出了警告,寫入卻照樣通過。hook 章節會講,是什麼讓「會執行的 hook」變成「會擋下的 hook」。我們接上的第一個文件 server 什麼都沒改變,真正有回答的是該工具自己的文件 server。另外,大多數人照抄的 hook 做法還需要一個獨立的腳本檔,我們的則完全放得進設定檔。
在寫下這四個檔案之前,你得先知道工作階段已經載入了什麼。
這四種是什麼,以及你的工作階段已載入什麼
Claude Code 擴充功能,是你加進 repo 的檔案,讓你不必再把同樣的指示重打進提示框。一共有四種,已經熟悉的讀者可以直接跳到下一節。
- skill 是歸檔在你 repo 裡的作業手冊。Claude 只在那項工作出現時才載入它的步驟,你也可以輸入它的名稱來啟動。
- hook 就像 git 的 commit hook,只是對象是 Claude 自己的動作。它是一支在事件發生時執行的腳本,例如一次編輯,不管 Claude 原本打算做什麼。
- subagent 是一位桌面空無一物的外包人員。它拿到你的簡報,自己去閱讀,然後交回一份報告,而不是那一大堆資料。
- MCP server 是 Claude 可以呼叫的 API 用戶端,用來取得不在你磁碟上的資料,例如資料庫或某個工具的文件。
要測量任何東西,都得先有基準線。開啟一個全新的工作階段,執行 context 指令。它會印出一張表,列出在你說第一個字之前就已載入的所有東西。在我們的測試環境中,總量是 26.2k token,視窗為 1m,而且完全沒有記憶檔那一行,也沒有 MCP 那一行。從現在起,你交給 Claude 的每一項工作,不是在這張表上多加一行,就是不動它。
第一項工作:發佈檢查清單變成 skill
skill 是歸檔在自己資料夾裡的檢查清單,只有你呼叫時 Claude 才會讀。第一項工作是一份你一直貼進提示框的發佈檢查清單:型別檢查、測試、升版號、加一行 changelog、commit,然後打 tag。
最顯眼的位置是記憶檔,但那是錯的選擇,因為那個檔案裡的所有內容,每個工作階段的每一輪都會載入。我們把步驟放進去,再讀一次 context 表。它多了一行 210 token 的 Memory files,不管你有沒有在發佈都會載入。文件也是這麼說的:當那個檔案的某一節長成了一套程序,它就該搬進 skill。
正確的選擇是在 skills 目錄下建一個資料夾,把步驟寫在 .claude/skills/release/SKILL.md,頂端放一段簡短說明,再加一行 frontmatter:disable-model-invocation: true。這一行會讓 skill 不出現在 Claude 自己的清單裡,只有你能啟動它,而這正是會 commit 和打 tag 的工作所需要的。之後你輸入斜線和名稱,檢查清單就會執行。
| context 讀數 | 總量 | Memory files 那一行 |
|---|---|---|
| 基準線,全新工作階段 | 26.2k / 1m | 無 |
| 檢查清單放在記憶檔 | 26.4k / 1m | 210 token |
| 檢查清單做成 skill | 26.2k / 1m | 無,skill 未列出 |
實際執行時,Claude 在一分鐘內完成了全部六個步驟:型別檢查、34 個測試、版號從 0.4.0 升到 0.4.1、一行 changelog、commit 和 tag。它還抓到我們自己清單裡的一個缺陷。照寫的 commit 指令會略過未追蹤的檔案,所以 Claude 先把新的 changelog 加進暫存區,並且說明了這件事。現在發佈變成一個你輸入的指令,它存在 repo 裡,整個團隊都能用,而在你輸入之前,不佔任何 context。
第二項工作:必須成立的規則變成 hook
hook 是 Claude Code 在工具觸發之前執行的腳本,這裡指的是任何編輯或寫入之前,而這支腳本的回應決定接下來會發生什麼。第二項工作是一條絕不能破的規則:在這個 repo 裡,儲存的資料檔絕不手動編輯,每一次變更都要經過 migration。
把這條規則用大寫寫在記憶檔裡,並不能強制執行它。文件說得很直接:那個檔案是 context,不是強制性的設定,像「never edit this file」這樣的指示是請求,不是保證,而擋下編輯的 hook 才是強制。
最顯而易見的第一個 hook,就是我們寫的那個。它在設定檔裡監看對資料檔的寫入,印出一則警告,然後以代碼零結束,就像一支記錄用的腳本。接著我們要求 Claude 直接在檔案裡修改一筆請款金額,不走 migration。hook 執行了,警告也印出來了,但寫入還是通過:第三張請款單的單價從 8000 變成 12000。Claude 的回覆完全沒提到 hook,因為一個乾淨結束又什麼都不印的 hook 沒有做出任何決定,它的警告也永遠不會傳到 Claude 那裡。
保證不在於 hook 有沒有執行,而在於它回傳的答案。修正的方式是保留同一個 hook,讓它回傳 deny 決定:一行簡短的 JSON,帶著你希望 Claude 讀到的理由。以代碼二結束也會同樣擋下。整個 hook 就是 .claude/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'"
}
]
}
]
}
}
同樣的要求,這次編輯被拒絕了,資料檔毫髮無傷。Claude 看到的錯誤是:PreToolUse:Edit hook error: Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions). 它表示什麼都沒改,並問我們要覆寫這道防護,還是改寫一個 migration。
限制是 Claude 自己點出來的:這個 hook 只監看編輯和寫入工具,而在之後的一次執行中,Claude 提議改用 shell 來完成變更。hook 必須連 shell 也一併涵蓋,或是讓 shell 不在你的允許清單上,因為只守一扇門的防護,仍然只是請求。這個 hook 不佔任何 context,因為它在你的對話之外執行。
第三項工作:灌爆對話的稽核變成 subagent
subagent 是一個獨立的 Claude 工作階段,擁有自己空白的 context。它收到你的簡報,讀取它需要的內容,只有摘要會回來。第三項工作是會灌爆你對話串的稽核:檢查原始碼資料夾下的每個檔案是否符合 README 的慣例,並列出每一處違規的檔案與行號。
直接在主對話裡問,Claude 會把每個檔案都讀進你的對話,而這些內容會在整個工作階段中一直留著。在我們的執行中,Claude 讀了全部 19 個原始檔,context 表的 Messages 那一行暴增,結果也被埋在那一堆內容底下。
正確的選擇是 .claude/agents/ 下的一個檔案,這裡是 .claude/agents/conventions-auditor.md,裡面有名稱、何時使用的說明、可以使用的工具,以及固定的模型。固定模型很重要,因為 subagent 否則會繼承你工作階段所用的模型,而大多數閱讀型工作並不需要那個大模型。
我們把完全相同的問題再問一次,Claude 還是自己讀檔。兩次嘗試中,即使它已列在清單上,Claude 都沒有呼叫這個 agent:2 次中委派 0 次。有效的做法是在提示開頭指名它:「use the conventions auditor subagent」。結果只發出一次呼叫,回來的是一份簡短的摘要。
| 執行 | 總 context | Messages 那一行 |
|---|---|---|
| 直接稽核 | 46.1k | 19.3k |
| 指名 subagent | 34.1k | 7.2k |
發現的問題品質相當:subagent 抓到了直接稽核漏掉的一個路徑 bug,也漏掉了直接稽核找到的一個。所以是檔案讓 subagent 存在,是你的提示讓 Claude 去用它;在 Claude 自己會選它之前,你得指名。還有一個限制:帳單沒有變。兩次執行花費相同,因為閱讀仍然會發生,只是不在你的視窗裡。
第四項工作:Claude 碰不到的資料變成 MCP server
MCP server 是 Claude 可以呼叫的 API 用戶端,它存在的目的,是提供比模型還新的資料,或是放在你磁碟之外的資料。這裡的直覺選擇是錯的,所以我們先選了它。
我們為測試用的函式庫接上一個公開的文件 server,並請 Claude 寫一個使用 fake timer 的測試。什麼都沒改變:Claude 本來就認識那個函式庫,而且只有在我們指名時才會呼叫該 server。模型已經知道的文件,並不是它碰不到的資料。
真正的測試,是問一個關於 Claude Code 本身的問題,關於一個比模型還新的 hook 設定:哪個設定會讓失敗的 hook 擋下動作、是哪個版本加入的、確切的 JSON 是什麼。沒有 server 時,Claude 讀了自己安裝版本的設定 schema,什麼都沒找到,然後回答說它找不到那個設定,也不會亂猜。這個回答誠實卻沒用,因為那個設定確實存在,只是它是在模型的知識之後才出現的。
修正的方法,是把 Claude Code 自己的文件 server 加進 repo,並使用 project 範圍,這樣設定會寫進 repo 根目錄的一個檔案,你的整個團隊都會拿到:
claude mcp add --transport http claude-docs --scope project https://code.claude.com/docs/mcp
這會寫出 .mcp.json。Claude Code 會把這個 server 列為 pending,直到你在下一個工作階段核准它,之後它的工具就能供 Claude 使用。我們用平常的說法問了同一個問題,沒有指名 server。Claude 搜尋了文件、找到頁面,並回答了設定、版本,以及要加入的 JSON,還附上連結。
那個設定是 "onFailure": "block",它讓失敗的 hook 擋下動作,而不是放行。它是在 v2.1.295 加入的,而我們安裝的還是 2.1.294。這就是限制:Claude 從未說我們安裝的版本無法使用,因為它沒有檢查版本,所以版本要你自己檢查。在 context 表中,這個 server 只會以工具名稱出現,並且在 Claude 去用其中一個之前,都是按需載入。
四個檔案,一張選擇卡
四項工作做完後,git status 恰好顯示四個檔案,因為 hook 不需要自己的腳本:
.claude/skills/release/SKILL.md.claude/settings.json.claude/agents/conventions-auditor.md.mcp.json
把它們 commit 之後,這個 repo 的下一個工作階段就擁有全部這些檔案,任何 clone 它的人也是。
每次選對的原因,是文件附帶的一張表,你可以把它留作決策卡:
| 情境 | 擴充功能 |
|---|---|
| 你又要貼一次的程序 | Skill |
| 必須每次都發生、而且不用詢問的事 | Hook |
| 輸出多到灌爆對話、你之後不會再看的附帶工作 | Subagent |
| 你一直從 Claude 看不到的地方複製過來的資料 | MCP server |
這幾種也可以組合。文件把 skill 和 MCP server 配成一對:server 提供連線,skill 教 Claude 如何好好使用它。skill 也可以在分叉出來的 subagent 裡執行,讓它的閱讀留在你的視窗之外。當另一個 repo 需要同樣的設定時,plugin 就是把這些全部一起送出的那個盒子。
這張卡決定的是種類,不是品質。只記錄的 hook、你從不指名的 subagent,以及為模型已認識的文件而接的 server,都屬於對的種類,卻都失敗了。從卡上選出種類,再讓檔案真的給出回答,而不只是存在。
結論:何時用哪個、何時都不用、唯一的規則
你以前一直重打的四項工作,全都先用錯的方式試過,在畫面上失敗,現在都變成 repo 裡有效的檔案。發佈是一個你輸入的指令。資料規則是一個拒絕編輯、並讓 Claude 來詢問的 hook。稽核以摘要的形式回來。文件問題則得到了 Claude 自己找不到的設定與版本。
我們一開始用的那個 hook,也就是執行了卻放行寫入的那個,教會了貫穿這一切的規則:hook 只憑它的指令回傳什麼來做決定,所以只記錄的 hook,是穿著戲服的請求。
有些情況這幾種都用不上:
- Claude 一直做錯的慣例,仍然屬於記憶檔,以一行的形式,而不是一套程序。
- 需要你來回調整、而且要看著歷史紀錄的工作,留在主對話串,不要放進 subagent。
- 為模型已認識的函式庫接文件 server,只會耗掉 context,什麼都不會改變。要連的是模型不可能擁有的資料,而不是它已經有的。
要記住的規則:給你每一項重打的工作一個自己的檔案,從卡上選出種類,並且知道在這幾種之中,只有 hook 從不詢問。
AIDive