從『如果你想要』到 Tetris 的十二天
Claude Code Mods 是在 Claude Code 引擎內執行的 TypeScript 模組,以函式的形式掛鉤(hook)引擎的事件。帶領 Anthropic Claude Code 團隊的 Boris Cherny 用五個字宣布了這項功能:「Claude mods are landing now.」這則貼文收穫了 2,400 個讚,而當時已經有人在終端機裡做出了 Tetris。Tetris、Doom,還有一隻會在 Claude 執行你的測試時長大的寵物,全都畫在提示列上方,而且不消耗任何 token。
這則宣布貼文並未附上任何文件頁,而是連到一個由 Anthropic 工程師在 12 天前開的 GitHub issue,裡面寫著一個條件。Alice Poteat 寫道,社群的反應很可能會決定這項功能最終是否上線。177 則留言之後,人們已經把這個掛著旗標的執行檔操到極限,測量了它的逾時行為,甚至在上面做出了遊戲。
| 指標 | 數值 |
|---|---|
| 宣布貼文的讚數 | 2,400 |
| GitHub issue 的留言數 | 177 |
| 本週版本的型別介面規模 | 10,700 lines |
| 事件數(issue 討論串版本,依名詞分類) | 84 on 19 |
| 48 小時內出現在 GitHub 上的 Mod 數 | 31 |
截至本文寫成時,文件頁仍回傳 404。這篇文章會介紹擴充 Claude Code 的四種方式、一個刻意做出來又刻意弄壞的實用 Mod,以及在你把 Mod 上線之前,還有哪些地方仍不穩定。
Mod 是中間的一個函式
原始碼樹用一句話定義了它:Mod 是一個 Claude Code plugin,其行為寫在一個 hooks 模組裡。一筆註冊項目就能以函式的形式掛鉤引擎的事件。落到磁碟上,那就是一個 plugin 資料夾、一份剛好指定一個模組的 hooks manifest,以及該模組本身。
每個 hook 都是三個東西的函式:$,也就是所有副作用會經過的那扇門;事件本身;以及延續(continuation),也就是你下面剩下的鏈結。Hook 會像中介軟體(middleware)一樣層層巢狀。最先註冊的那個擁有這個事件,鏈結中更下面的任何 hook 都無法抑制它。這個順序是由設定決定的,而不是由安裝順序決定。用 Alice Poteat 的話來說,順序是設定出來的,不是照安裝的先後順序。
沒有任何環境級(ambient)存取權限。Mod 所做的一切都要經過 $,因此管理員可以稽核、允許、拒絕或記錄任何事件。有位留言者做了個總結:一個 plugin 做了什麼,就精確地等於它發出了哪些呼叫。在 * 上掛一個 hook 就能看到每一個事件,所以一個稽核日誌只需要一個函式。Mod 也可以繪製畫面,因為介面用的是 React。它在 Bun 上以同一行程執行,第 99 百分位的延遲只有 50 微秒。只要一個環境變數就能開啟它。至於 $ 上究竟會放什麼,目前仍在和搶先體驗的合作夥伴一起設計中。
擴充 Claude Code 的四種方式,該用哪一種
shell hook 是引擎在固定時機呼叫的腳本。它從標準輸入取得 JSON,並以 exit code 回應。回應有上限:傳回的上下文最多 8,000 個字元,部分 hook 則只有 2,000 個字元。用 Pratham 的話來說,在 Windows 上它們常會以很奇怪的方式故障。
plugin 是一個盒子。一份 manifest 就能把 skill、agent、hook 與 MCP server 打包在一起。它可以從市集(marketplace)安裝,也可以直接從磁碟上的資料夾載入,而且在任何東西執行之前,都有一個指令可以驗證這個盒子。
skill 是模型在需要時才讀取的文字說明。內文只有在符合的提示出現時才會載入。它仍然是改變行為最便宜的方式。
Mod 就是同樣的盒子,只是多了一個檔案:一份指定模組的 hooks manifest,而那個模組是 TypeScript 寫的、具型別、在行程內執行,並掛鉤每一個引擎事件。差別就僅止於此。
| 機制 | 是什麼 | 執行方式 | 上限 |
|---|---|---|---|
| Shell hook | 在固定時機被呼叫的腳本 | 子行程,以 exit code 回應 | 傳回最多 8,000 chars back(部分 hook 為 2,000) |
| Plugin | skill、agent、hook、MCP server 的組合包 | 安裝或從磁碟載入 | 執行前先驗證 |
| Skill | 符合提示時載入的文字說明 | 位於模型的上下文中 | 成本最低的改動方式 |
| Mod | Plugin 加上一個 TypeScript hooks 模組 | 行程內執行,涵蓋每個事件 | 具型別,搶先體驗 |
這些型別來自一個斜線指令(slash command),它會把 $ 提供的完整清單直接寫進你的專案裡。舊的 shell hook 是被包裝(wrapped)起來,而不是被淘汰。在一個早期版本上,Spencer Morley 就曾看過包裝器載入失敗,但宣告仍留在原處。經驗法則是:要改變 Claude 知道的東西,寫一個 skill;要在某個時機執行腳本,用 shell hook;要出貨一個組合包,用 plugin;要坐進引擎裡面,用 Mod。只要旗標是關閉的,shell hook 在任何地方都照樣能用。
Anthropic 自己的三個 Mod,原始碼解讀
有三個 Mod 內建於執行檔中,原始碼都在 GitHub 上:一個安全預設值、一個 diff 面板,以及遙測(telemetry)。
安全預設值位於最外層。在有受管理設定的機器上,或是在 Team 或 Enterprise 組織中,使用者安裝的任何東西都無法蓋過它。它掛鉤了 12 個事件,而每個 hook 都只會做三件事之一:放行使用者層級以外的呼叫、以名稱拒絕來自使用者層級的呼叫者,或是直接放行。它採用失效即關閉(fail closed)的設計,而原始碼裡的文件註解也只用兩個字說明了這件事。這正是討論串最在意的部分:管理員從 $ 上移除一項能力後,底下註冊的任何東西都無法再呼叫它。用某位留言者的話來說,這和「拜託一個 plugin 不要做某件事」在本質上完全不同。
Diff 是逐字稿旁的一個面板,逐檔顯示該工作階段中尚未提交的變更,並隨著 Claude 編輯即時更新。它在工作階段開始時就會註冊,程式碼橫跨 27 個輔助檔案,不是什麼玩具功能。
遙測會在引擎建立的過程中,為 $ 加上一個名詞。它會等待底下的結果,再把該結果連同自己一起回傳。目前只在內部版本中執行。
README 上寫著要從原始碼執行一個、從原始碼測試一個。但今天這個版本的說明清單裡只列出 validate、eval 與 details,清單上並沒有 test,儘管 test 的說明指令仍然有回應。內建的層級也會拒絕你自己的版本:如果你出貨的 plugin 用了這些名稱之一,執行檔會改載入它自己內建的那份。
四十二行:一個對模型隱藏機密的 Mod
在這個版本上,寫出型別的斜線指令會產生 11,700 行:23 個名詞、共 84 個事件。工具呼叫的 hook 只能以結果或拒絕來回應,永遠不能回傳原始文字,這部分是由核心固定死的。
這個 Mod 由三個檔案組成:plugin manifest、只有一行的 hooks manifest,以及模組本身。模組只有 42 行,它會等待底下的結果、清洗過後再交回去。四個樣式(pattern)就涵蓋了兩種供應商金鑰格式、一個 GitHub token,以及任何被指派給名為 key、secret 或 token 的變數的值。
validate 會在執行前先讀過原始碼,列出這個模組掛鉤了哪個事件,以及它在 $ 上呼叫了哪一項。唯一的警告是缺少作者資訊。在開啟旗標、從磁碟載入之後,測試檔裡放了兩把金鑰,兩把都是刻意造假的,而模型讀到的是「redacted」。用它自己的話來說,回傳的值已經被遮蔽,所以它看不到裡面實際的內容。
| 指標 | 數值 |
|---|---|
| 模組長度 | 42 lines |
| 呼叫延遲(含 worker) | 28 ms |
| 工作階段新增的 token 數 | 0 |
引擎會把這次呼叫記錄為「由 hooks 模組解析完成」。清單裡沒有任何 skill、沒有任何 agent,也沒有任何常駐項目。仍有一個限制存在:對模型隱藏,不等於對畫面隱藏。逐字稿仍然會顯示該工具實際印出的內容。Ship Notes 的 Max 用一句話指出,那得靠另一個 Mod 才能解決。
打破它:太慢的防護就是被繞過的防護
同一個 Mod,只加了一行:在呼叫底下的內容之前先睡眠(sleep)15 秒。10 秒之後,引擎就放棄等待,回報這個 hook 已超出預算並被標記為 skipped,底下的內容則取而代之執行。指令照樣執行,並印出「hi」。
把 sleep 換成 throw:574 毫秒後得到同樣的結果——skipped,指令照樣執行。
| 情境 | 時間 | 結果 |
|---|---|---|
| 正常的 hook | 28 ms | Resolved |
| 拋出例外 | 574 ms | Skipped,指令仍執行 |
| 卡死 | over 10 s | Skipped,指令仍執行 |
兩種失敗最後都落在同一個詞上:skipped。討論串一週前就已經測量出這種不對稱性。載入時缺失的能力會失效即關閉(fail closed);而超出預算的 hook 則會失效即開放(fail open)——用 Spencer Morley 的話來說,就是「吵歸吵,但照樣被繞過」。一個沒有理由的攔阻,只會讓模型改去用另一個工具。Pratham 就親眼看過模型改選了另一個工具,照樣把檔案寫了下去。
目前檯面上的解法是加一個 catch。Alice Poteat 提議在 hook 的回傳上加一個 catch,只要你拖太久或拋出例外,它就會被執行。一次被改寫過的寫入動作,仍然需要留一則給模型的說明:出於快取的原因,模型看到的還是它原本要求寫入的內容,所以你得附加一行上下文說明。這個時間預算同時也是隔離機制:hooks worker 是獨立執行的,一旦它當機,引擎會重新啟動它,並在這個工作階段中把 hook 全部關閉。真正的修法不是拉長預算,而是宣告一個 catch。
Mods 上線四十八小時,一句話能換到什麼
上線兩天後:提示列上方出現了 Tetris,還有另外七款遊戲,可以在 Claude 工作時遊玩,而且完全不消耗 token。1993 年的原版 Doom 在自己的行程中執行,Mod 透過本機 HTTP 連上它,並以每秒 10 次的頻率重繪畫面。轉圈圖示裡還藏了一個呼吸節奏器,以及一個在 hooks worker 內執行、擁有 26 萬個參數的故事模型,同樣完全不耗用 API token。
登錄庫用 validate 掃描了 31 個 Mod。其中 14 個能執行主機行程,13 個能看到每一次工具呼叫。
社群討論串裡的第 8 號示範宣稱,一句話就能寫出一個能在模型讀取之前先隱藏機密的 plugin。我們也試著要求了一個。
| 指標 | 數值 |
|---|---|
| 花費時間 | 4 min |
| 對話輪數 | 34 |
| 花費 | $1.23 |
| 行數(含測試檔) | 190,對照我們寫的 42 行 |
| 測試 | 4 個測試在三分之一秒內全部通過 |
它會依類別標示自己遮蔽了什麼,通過 validate 檢查且無警告,也不需要任何 API 金鑰或模型呼叫。有一則回覆這樣形容:允許清單(allowlist)才是產品,Tetris 只是展示品。
結論:現在就動手寫,晚點再上線
登錄庫自己的數字就是警訊。31 個 Mod 中有 14 個能執行主機行程,而目前唯一的審查方式僅是靜態足跡分析。eval 指令的說明本身也這麼寫:測試套件通過,不代表通過了安全審查。
| 訊號 | 數值 |
|---|---|
| 8 日的事件數 | 20 |
| 15 日的事件數 | 84 |
| 兩週內釋出的 CLI 版本數 | 14 |
| Hacker News 分數 | 2 |
| 相容性問題的回覆數 | 0 |
如同 Marat 所指出的,那 10 秒的時間預算活在執行時期(runtime)裡,而不是寫在型別定義中。目前沒有任何 changelog 紀錄、沒有文件頁,也沒有正式的上線貼文。有競爭者宣稱 Anthropic 抄襲了這個概念:Ahmad Awais 指出 Command Code 的 mods 在先,而且是他親自寫了 Command Code 的範例 mod,這點值得列入考量。那則寫著「搶先體驗,API 可能會變動」的貼文獲得了 357 個讚。
如果你已經在寫 hooks,而且想要稽核、遮蔽機密或是一個面板,現在就可以動手做一個。如果你要出貨給團隊使用,那就等到這份契約(contract)真正寫定之後再說。也正因為這樣的開放性,那個會失效即開放的時間預算、MCP 呼叫的型別不匹配,以及包裝器失效的問題,才會在短短幾天內就被使用者找出來。
AIDive