AI 工程

Claude Mods 實戰:用幾十行 JavaScript 改造 Claude Code 的介面與行為

Claude Code 用久了,你一定碰過這幾種時刻:想在 spinner 旁邊看到這輪已經呼叫了幾次工具;想讓 git push --force 之前先跳出一個「確定嗎」;想在每輪結束後自動檢查 Claude 有沒有偷工減料。以前這些需求只能拆給 settings hooks、status line、skill、MCP server 各做一塊,而且全都是「從外面」接進 Claude Code。

Anthropic 在 Claude Code 推出的 Mods,是第一個讓你把程式碼直接放進 Claude Code「裡面」跑的機制。Latent Space 上 Dan McAteer 寫的〈Claude Mods: Flipping the Harness Inside Out〉(2026/10/21)把它形容成「agent harness 的 middleware」:harness 過去是面向機器設計的,mods 讓它開始面向坐在螢幕前的人。

這篇不談願景,直接帶你把 mod 用起來:它怎麼運作、第一個 mod 怎麼寫、怎麼叫 Claude 幫你寫、哪些現成的值得裝、以及哪些坑要先知道。

一、Mods 到底是什麼

官方文件的定義很短:mod 是一種 plugin,由 JavaScript 或 TypeScript 的 event handler 組成,用來改變 Claude Code 的外觀與行為。

關鍵差別在「跑在哪裡」。文件拿它跟其他擴充方式對比:settings hooks、skills、status line、MCP server 都是從 Claude Code 外部運作;mod 跑在 Claude Code 內部,所以能做到前者做不到的事,例如:

  • 在對話旁邊開一個常駐的 pane,或在輸入框上方放一條 band,裡面有分頁、按鈕、輸入框
  • 改寫或重新設計 Claude Code 自己畫的東西:工具呼叫那一列、spinner、Claude 問你問題的對話框(唯一不能改的是權限確認提示)
  • 新增一個 /指令,按下去立刻執行你的函式,不需要等 Claude 跑一輪,Claude 正在工作時也能用
  • 攔截、改寫或直接回答工具呼叫、使用者輸入、模型請求

版本需求(依文件):終端機 Claude Code v2.1.287 以上、Desktop App 的 Code 分頁 v2.1.286 以上、VS Code 擴充 2.1.299 以上。先跑一次 claude --version 確認。

Claude Code 本身就內建了六個 mod,文件列出的包括:載入 AGENTS.md 當專案指示的 cc-plugin-agents-md、接管 /diff 並畫出差異面板的 cc-plugin-diff、提供撰寫 mod 技能的 cc-plugin-plugin-authoring、保護組織管理設定的 cc-plugin-sec-default、送出分析紀錄的 cc-plugin-telemetry,以及預設關閉、會在輸入框上方顯示提醒的 side agent cc-plugin-you-should-know。其中幾個的原始碼公開在 anthropics/claude-code repo 的 mods/ 目錄,是很好的參考範本。

二、運作原理:事件、洋蔥式 handler、$ API

Claude Mods 運作原理:事件流經 mod 的 hook,每個 hook 可以放行、改寫或直接回答

整個機制只有三個概念,搞懂就能讀懂所有範例。

1. 事件(event):Claude Code 裡幾乎每件事都會發出一個具名事件。常用的幾類:

類別 代表事件 什麼時候觸發
工具 tool.call、tool.check 工具要執行前;決定是否允許執行時
輸入 prompt.submit、prompt.context 你送出訊息時;組裝 Claude 要讀的內容時
指令 command.run 你輸入 mod 註冊的 /指令
回合 turn.start、turn.step、turn.complete 一輪開始、每次模型請求、一輪結束
工作階段 session.start、session.compact mod 載入後第一個 prompt 之前;壓縮上下文時
介面 ui.render、ui.press 某個畫面元件要繪製時;你按了 mod 畫的按鈕時
子代理 agent.spawn 要開 subagent 時

另外還有 classic.* 系列,對應原本 settings hooks 的事件(例如 classic.Stop、classic.PostToolUse),讓舊的 hook 邏輯可以搬進 mod。

2. Handler 簽章 async ($, e, next):這是典型的 middleware 洋蔥模型。每個 hook 拿到事件 e,有三種選擇:

  • 放行(observe):看一眼就 return next(e),例如計數、記 log
  • 改寫(rewrite):return next({ ...e, 某欄位: 新值 }),例如幫 spinner 加後綴、幫 prompt 補上下文
  • 回答(answer):不呼叫 next,直接回傳結果,例如 { deny: '原因' } 擋掉工具呼叫,或 { result } 代替工具回應

on() 的第二個參數可以放 matcher 縮小範圍,例如 on('tool.call', { tool: ['Edit', 'Write'] }, hook);欄位可以是字串、陣列或 regex。

3. $:mods API:hook 本身之外要做的事都透過 $:$.ui.open() 開 pane、$.ui.toast() 跳通知、$.ui.status() 在輸入框下方寫一行、$.command.register() 註冊指令、$.tool.register() 給 Claude 一個新工具、$.model.complete() 或 $.model.fork() 呼叫模型、$.fs.* 讀寫檔、$.process.run() 跑外部程式、$.http.fetch() 打網路、$.store.* 存跨 session 的 JSON。

有個設計很漂亮:每個 $ 方法本身也是事件(例如 fs.read、model.complete),所以先載入的 mod 或組織的安全設定,可以觀察甚至拒絕後面 mod 的檔案、程序、網路存取。

三、手把手:寫出你的第一個 mod

以下是官方教學裡的 first-mod:計算 Claude 的工具呼叫次數、顯示在 spinner 旁,再加一個 /tally 指令。目錄結構:

first-mod/
├── .claude-plugin/
│   └── plugin.json
└── hooks/
    ├── hooks.json
    └── register.js

.claude-plugin/plugin.json 是 plugin 的 manifest:

{
  "name": "first-mod",
  "version": "0.1.0",
  "description": "Counts Claude's tool calls, shows the count beside the spinner, and adds a /tally command",
  "author": { "name": "Your Name" }
}

hooks/hooks.json 指向你的程式:

{
  "description": "The first-mod hooks module",
  "modules": ["./register.js"]
}

hooks/register.js(文件稱為 hooks module,必須是 ES module 並 export register):

let calls = 0

export function register(on) {
  on('session.start', async ($, e, next) => {
    await $.command.register({
      name: 'tally',
      description: 'Show how many tool calls Claude has made',
    })
    return next(e)
  })

  on('tool.call', async ($, e, next) => {
    calls += 1
    $.ui.invalidate('ui.render')   // 要求重畫,讓新數字顯示
    return next(e)                 // 放行,工具照常執行
  })

  on('command.run', { command: 'tally' }, async () => {
    return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
  })

  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
    // 保留原本的 spinner,只在後面加上計數
    return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
  })
}

這段程式剛好示範了三種模式:tool.call 是放行、ui.render 是改寫、command.run 是直接回答。JS 和 TS(.ts、.tsx 等)都能直接載入,不需要 build。

接著四個指令,照順序跑:

# 1. 靜態檢查:會列出這個 mod 掛了哪些 hook
claude plugin validate ./first-mod
#   > ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
#   ✔ Validation passed

# 2. 只在這個 session 載入,存檔即熱重載
claude --plugin-dir ./first-mod

# 3. 在 session 裡確認:/plugin → Installed 分頁;改了檔可用 /reload-plugins

# 4. 跑測試(在 mod 目錄下執行 *.test.ts)
claude plugin test

測試框架是內建的 claude-code/testing,可以假裝工具呼叫、再呼叫你的指令驗證輸出。文件的範例是先用 on('tool.call', () => ({ result: 'ok' })) 攔下所有工具(不真的執行),觸發兩次 $.tool.call(),再檢查 /tally 回傳「2 tool calls」。把 mod 當成一般程式碼寫測試,是後面放心分享給團隊的前提。

開發期間一直用 --plugin-dir 就好。文件特別提醒:安裝後的 plugin 會依版本快取,改了程式要升 version 再重裝才會生效。

四、更省事:直接叫 Claude 幫你寫

Mods 最實用的入口其實是 Claude 自己。內建的 plugin-authoring skill 就是為此存在,你只要在互動 session 裡用白話描述:

make a mod that shows the current git branch above the prompt

或用中文也行,重點是講清楚三件事:畫在哪裡、什麼時候觸發、要不要呼叫模型。例如:

/plugin-authoring
幫我寫一個 mod:每一輪結束時,如果這輪有改到 *.sql 檔,
在回答下方顯示一行提醒「記得跑 migration dry-run」。
不要呼叫模型,只看這輪的工具呼叫紀錄。寫完附一個 test。

依文件,Claude 會把檔案寫到 ~/.claude/dev-mods/<session-id>/<mod-name>/。在 default 與 acceptEdits 權限模式下,mods 資料夾是受保護路徑,每個檔案都要你核准;第一次存檔時會問你要「Enable for this session」(熱重載)還是「Not now」。

注意兩件事:Claude 寫的 mod 只在當下這個 session 生效;而且 cleanupPeriodDays 到期後,session 的 mods 資料夾會被清掉。滿意的話記得複製出來,例如 cp -r ~/.claude/dev-mods/<session-id>/git-branch ~/mods/,之後用 claude --plugin-dir ~/mods/git-branch 載入,或放進團隊 marketplace。

五、三個自己寫的實用 recipe

以下三個是依文件事件定義組出來的骨架,沒有實際跑過;事件欄位名稱請以你版本產生的 claude-code.d.ts 為準(用 --plugin-dir 在互動 session 載入時,型別檔會產生在 .claude-plugin/types/)。

Recipe 1:推到 main 之前強制確認

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  if (/git\s+push.*\bmain\b/.test(e.command ?? '')) {
    return { decision: 'ask', reason: '要推到 main,確認一下' }
  }
  return next(e)
})

文件自己也提醒:比對指令文字不等於真正的防護,換個寫法就繞過了。真正要保護的分支,請在 GitHub/GitLab 上設 branch protection,mod 只是第一道提醒。

Recipe 2:每輪結束顯示耗時

on('turn.complete', async ($, e, next) => {
  if (e.isAborted) return next(e)
  return { text: '⏱ ' + Math.round(e.durationMs / 1000) + ' 秒' }
})

turn.complete 回傳 { text } 會在答案下方加一行;事件也帶有 e.usage,可以順便顯示 token 用量。

Recipe 3:送出前自動補上下文

prompt.submit 可以回傳 next({ ...e, context }) 幫訊息加上背景資訊,例如把 $.process.run(['git', 'status', '--short']) 的結果附上去。注意 $.process.run 吃的是參數陣列、不經過 shell,預設 30 秒逾時。

六、值得先裝來玩的現成 mod

Anthropic 官方範例(anthropics/claude-code-playground 的 claude-code/mods/,官方註明「照原樣分享、不提供支援」):

  • token-weather:在輸入框上方畫出 context window 的即時「天氣預報」
  • blast-radius:攔下高風險 shell 指令(rm -rf、git reset --hard、force push、migration),先顯示會影響什麼,再給 Proceed/Cancel 按鈕
  • replay-theater:新增 /replay,一個 diff 一個 diff 回放上一輪 Claude 改過的檔案
git clone https://github.com/anthropics/claude-code-playground.git
cd claude-code-playground/claude-code/mods
claude --plugin-dir ./token-weather

Dan McAteer 的 ModSmith(GitHub DannyMac180/skills 的 modsmith/):一組現成 mod,加上估算成本與審查別人 mod 的工具。

/plugin marketplace add DannyMac180/skills
/plugin install modsmith-all@modsmith

裡面最有代表性的是 Next Steps Supervisor:只在「真的做了事」的回合(改了檔、跑了指令或 8 次以上工具呼叫)之後,開一個 side check 問:目標是什麼、達成了沒、哪裡偷工減料、有沒有需要你核准的事,然後把下一步做成可以點的按鈕;/supervisor 可關閉。其他還有每輪出 2 到 3 題小測驗的 quiz-after、讓 Claude 登記假設的 assumption-ledger、切換 ui/api/review 模式的 effort-modes。README 註明目前只有 quiz-after 與 next-steps-supervisor 在 Desktop App 實際跑過,其他還沒。

七、數據與限制:先看清楚再裝

成本是真的。 $.model.complete 與 $.model.fork 會算在你的方案、API key 或雲端帳單上。以 Supervisor 為例,Dan McAteer 在文章中引用 ModSmith README 的估計:在 50k token 的 session 裡,每次檢查約 5 到 6k「等效輸入」token;README 的另一處寫法是一次走 cache 的 fork,約 250 token 問題加 100 到 300 token 輸出。兩者都是作者估計,不是基準測試。effort-modes 每次切換會重建一次 cache,README 記錄在 37k token 的 session 中量到約 10k token。

觸發門檻比想像中常被滿足。 McAteer 從 10/15 裝上 Supervisor 後統計,111 個回合中有 99 個達到觸發條件,他的原話是「實務上,幾乎每一輪」。設計 mod 時要先想好噪音預算,不然很快就會被你自己的 mod 煩到關掉。

沒有沙盒。 文件寫得很直白:mod 是以你的權限執行的程式碼,而且不在沙盒裡;就算開了 sandbox,被隔離的是 Claude 執行的 Bash 指令,mod 啟動的程序在沙盒外。裝別人的 mod 等於執行別人的程式。

失敗預設是放行。 每個 hook 有 10 秒執行時間(prompt.edit 只有 50 毫秒);拋錯、逾時或回傳格式錯,如果發生在呼叫 next 之前,該 hook 會被跳過。所以當作防護用的 mod,必須用 .catch() 讓它 fail closed。

其他硬限制(依文件):$.ui.invalidate 每秒最多 10 次(終端機 30 次);$.fs 單檔 4 MiB;$.http.fetch 單次回應 4 MiB、最多 5 次轉址、總共 30 秒;$.store 全機器所有 session 共用、總量 4 MiB、寫入非原子。你按 Esc 停止後,背景呼叫會被拒絕,直到你再次輸入。

不載入的情況:不受信任的工作區、--safe-mode、--bare、設定了 disableAllHooks,或組織的 managed settings 擋掉時,mod 都不會載入。出問題時 claude --safe-mode 是最快的除錯開關。

八、什麼時候用 mod,什麼時候別用

Mods 與其他擴充方式比較:mod 跑在 Claude Code 內部,其他方式從外部接入

  • 只是想讓 Claude 多懂一套流程:寫 skill 就好,不需要程式碼
  • 想接外部系統的資料或動作:MCP server 依然是標準做法,而且跨 agent 可用
  • 只要在底部顯示一行狀態:status line 最簡單
  • 要在工具執行前後跑腳本、跨工具相容:settings hooks 足夠
  • 要畫互動介面、改 Claude Code 自己的元件、在不佔 Claude 回合的情況下跑指令、或改寫模型請求:這才是 mod 的主場

一個實用判斷:如果你發現自己在 status line 腳本裡塞狀態、又在 hook 裡寫暫存檔互相溝通,那就是該換成 mod 的訊號。

九、給工程團隊的落地清單

  1. 裝任何 mod 前先跑 claude plugin validate。輸出的 hooks: 與 calls: 會告訴你它掛了哪些事件、會呼叫哪些 API(檔案、程序、網路、模型)。ModSmith 也附了 bun scripts/audit-mod.ts <mod> 審查腳本可以搭配。
  2. 團隊共用走 marketplace,開發走 --plugin-dir。名稱別用 claude- 開頭,validate 會擋看起來像 Anthropic 官方的名字。
  3. 管理者先設好 managed settings。文件列出的開關包括 allowManagedModsOnly(只允許組織派發的 mod)、disableSideloadFlags(禁止 --plugin-dir/--plugin-url)、prependPlugins/appendPlugins(控制載入順序)。載入順序是:安全守門 mod、prepend、使用者 mod、append、內建 mod。
  4. 防護類 mod 一律 fail closed、而且不當唯一防線。用 .catch(),並在 Git 平台、CI 設真正的權限。
  5. 每個呼叫模型的 mod 都寫下成本與關閉方式。學 ModSmith 的做法:每個 mod 附 DECISIONS.md,標明每輪 token 成本、對 cache 的影響、怎麼關。
  6. 從一個小痛點開始。先寫一個不呼叫模型的 mod(計數、提醒、快捷指令),熟悉事件與 $ API,再進到會 fork 對話的 side agent。

Mods 真正改變的,是你不用再等官方加功能:工作流裡哪個環節讓你分心、讓你不放心,就自己寫幾十行把它補起來。

來源

整理:DataAgent · Coding Agent 實戰教學

發表迴響

%d 位部落客按了讚: