生活分享
Claude Code|建立第一個 mod:在 Claude Code 行程內數工具呼叫
寫一個三檔案的 mod,用驗證器與測試確認它掛上的事件。文件把 mod 定義成多了入口檔的 plugin:入口檔叫 hooks module,Claude Code 在事件發生時呼叫裡面的函式,函式可以觀察、改寫或接手事件。
閱讀時間約 10 分鐘

進階 · CLI / Desktop
設定檔 Hook 在 Claude Code 外部執行。想在行程內數工具呼叫、把次數顯示在 spinner 旁,文件的做法是 mod。本篇照官方教學建立 first-mod:三個檔案、四個事件函式,再用 claude plugin validate 看 Claude Code 讀到什麼、用 claude plugin test 寫不需要 session 的測試。交付是能載入的 first-mod、一個通過的測試,與驗證器的 hooks、calls 兩行紀錄。
先讀 Plugins 入門Claude Code|Plugins 安裝與管理安裝、啟用、更新與移除外掛。Plugin 可以把 Skills、代理、Hooks、MCP 或語言伺服器相關設定打包,方便安裝與管理。本篇會從官方目錄查看一個外掛的內容、選擇作用範圍、完成一次能力驗證,再練習停用與移除。安裝成功只是開始,還要確認依賴、登入與實際工具能否使用。閱讀全文、建立第一個 HookClaude Code|建立第一個 Hook設定事件與條件,確認 Hook 實際觸發。本篇會建立第一個 Hook:Claude 使用檔案編輯工具後,將事件名稱與工具名稱寫入練習專案的本機紀錄。你會先人工測試腳本,再接上 PostToolUse,最後用真實編輯確認觸發。這個起點能分清楚「程式可跑」與「Claude 真的呼叫過它」。閱讀全文與Plugin 打包Claude Code|把 Skills 與 Hooks 包成可版本管理的 Plugin讓同伴安裝、升級與回退同一套工作流程。當一個 Skill 需要連同範本、參考文件和 Hook 分享給同事,逐個複製檔案容易漏件。本篇把差異審查流程整理成可辨識版本的本機 Plugin,驗證命名空間、腳本路徑、更新與停用,最後交付一個可搬到另一個資料夾使用的完整目錄。閱讀全文。下載第 97 篇材料,在 starter 的 first-mod 操作:它只有 plugin.json 與 hooks.json,其餘由你寫,reference 有四個檔可對照。需要 Claude Code v2.1.287 或更新版本(用 claude --version 檢查);互動試用需要你自己登入的 Claude Code,validate 與 test 不需要 session。閱讀約 20 分鐘,實作約 40 分鐘。
mod 是什麼,和設定檔 Hook 差在哪
文件把 mod 定義成多了入口檔的 plugin:入口檔叫 hooks module,Claude Code 在事件發生時呼叫裡面的函式,函式可以觀察、改寫或接手事件。
用詞先講清楚:Claude Code 把兩種都叫 hook;mods 文件裡單說 hook 指 mod 的事件處理函式,設定檔那種叫 settings hook。本篇固定稱後者「設定檔 Hook」、前者「mod 的事件函式」。設定檔 Hook 在行程外運作、不能畫進介面;mod 在行程內,可畫 pane 或改寫 spinner。
寫三個檔案
plugin.json 是一般的 manifest,mod 沒有額外必填欄位。hooks.json 的 modules 是只放一個相對路徑的陣列,文件說有了它才算 mod。register.js 是入口。
first-mod/
├── .claude-plugin/
│ └── plugin.json
├── hooks/
│ ├── hooks.json
│ └── register.js
└── tests/
└── first-mod.test.ts
{
"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": "Mokaair tutorial" }
}
{
"description": "The first-mod hooks module",
"modules": ["./register.js"]
}
// The count, shared by the hooks below
let calls = 0
// Claude Code calls this once when the mod loads
export function register(on) {
// Runs when the session starts, before your first prompt
on('session.start', async ($, e, next) => {
// Add the /tally command
await $.command.register({
name: 'tally',
description: 'Show how many tool calls Claude has made',
})
// Let the session start as usual
return next(e)
})
// Runs each time Claude is about to use a tool
on('tool.call', async ($, e, next) => {
calls += 1
// Ask Claude Code to draw the interface again, so the new count shows
$.ui.invalidate('ui.render')
// Let the tool run as usual
return next(e)
})
// Runs when you type /tally, and only then, because of the matcher
on('command.run', { command: 'tally' }, async () => {
// The text to print in the transcript
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
// Runs each time Claude Code draws the spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, with the count added after its word
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}
session.start 註冊 /tally,tool.call 計數並要求重畫,command.run 只在 /tally 時回覆,ui.render 把次數接在 spinner 字後。
每個事件函式拿到三個引數:$ 是 mods API,能呼叫的方法都在上面;e 是凍結的事件資料,要改就傳複本;next 把事件交給後面的 mod 與 Claude Code 自己的行為。處理有三種:觀察(回傳 next(e))、接手(如 command.run,不呼叫 next,第二個引數 { command: 'tally' } 是 matcher)、改寫(如 ui.render,把複本交給 next)。
載入並試用
用 --plugin-dir 載入,只限這一次 session,不會安裝。
claude --plugin-dir ./first-mod
請列出這個資料夾的檔案,再讀 README。
文件說明的預期:Claude 工作時,spinner 字後有往上數的次數,如 Thinking · tool calls: 2…;完成後輸入 /tally,transcript 出現 first-mod: Claude has made 2 tool calls since this mod loaded(次數是你自己的),plugin 名稱由 Claude Code 加在指令文字前。指令清單裡沒有 /tally,文件說是 module 沒載入。在提示列執行 /plugin,文件說分頁列下方有一行暗色字列出數量與名稱,如 1 mod active · first-mod;overview 也用這一行確認以 --plugin-dir 載入的範例 mod。
不開互動 session 也能檢查指令:
claude -p "/tally" --plugin-dir ./first-mod
文件列的預期輸出是 first-mod: Claude has made 0 tool calls since this mod loaded。
以上都是文件的預期。本站只在容器用 Claude Code 2.1.289 跑過 validate 與 test,沒跑互動 session,也沒跑 claude -p。請自己寫 observation.md:版本、spinner 原文、/tally 與 claude -p 的輸出、/plugin 那一行;不同就照實寫。
改一行字,看熱重載
session 不關,把 ui.render 的 ' · tool calls: ' 改成 ' · tools used: ' 並存檔。文件說預期 transcript 出現一行,說 first-mod 已重載並列出它的事件函式,下一次 spinner 變成 Thinking · tools used: 1…;請記下原文。
// Runs each time Claude Code draws the spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, with the count added after its word
return next({ ...e, props: { ...e.props, suffix: ' · tools used: ' + calls + '…' } })
})
文件說 Claude Code 會監看 --plugin-dir 載入的目錄,檔案一改就熱重載 hooks module;每次重載都再執行 register,所以 calls 歸零,/tally 從頭數。要跨重載保留數值,文件指向 Draw in the interface 頁的「Keep state」一節。
用驗證器看 Claude Code 讀到什麼
claude plugin validate 檢查 manifest,並對 hooks module 的原始碼跑 Claude Code 載入 mod 時的同一套靜態分析,不執行程式也不開 session。本站在容器以 2.1.289 對本課材料執行過;輸出的行首符號是 > 與 √,文件範例是 ❯ 與 ✔,其餘文字相同:
claude plugin validate ./first-mod
> ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
> ./register.js calls: $.command.register, $.ui.invalidate
√ Validation passed
hooks: 行是掛上的事件與篩選,calls: 行是呼叫的每個 mods API 方法。文件說事件沒列在第一行,Claude Code 也不會呼叫那個事件函式;常見原因是事件名拼錯,例如拼成 tool.calls,命令會報 "tool.calls" is not an event。文件列的規則:
- mods API 呼叫要寫完整,如 `$.store.get('notes')`;`const ui = $.ui` 會報 `$.ui is used as a value`。
- 事件名要是字串字面值,用變數會報 `the event name passed to on() is not a string literal`。
- 只用相對路徑匯入 plugin 目錄內的檔案,唯一允許的裸匯入是 `claude-code`;寫成 ES module,用 `import` 不用 `require`。
文件說每次以 --plugin-dir 載入或重載 mod,Claude Code 會把依你版本產生的 .d.ts 型別檔寫進 mod 目錄的 .claude-plugin/types/;它們與任何頁面不一致時,以它們為準。
寫一個不用 session 的測試
測試檔名以 .test.ts 結尾。這個測試觸發兩次工具呼叫,再執行 /tally。
import { expect, test } from 'claude-code/testing'
test('/tally reports the tool calls the mod has seen', async ($, on) => {
// Answer each tool call in Claude Code's place, so no tool runs
on('tool.call', () => ({ result: 'ok' }))
// Fire two tool calls, which the mod's tool.call hook counts
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// Run /tally and check the text its hook returns
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})
測試的 $ 扮演 Claude Code,不是事件函式拿到的 mods API:它的每個方法觸發同名事件並送進 mod 的事件函式。$.tool.call(...) 先經過 mod 的 tool.call 事件函式,再交給 stub:on('tool.call', () => ({ result: 'ok' })) 代替 Claude Code 回答,所以沒有 ls 真的執行。stub 要在第一次呼叫 $ 前登記。
claude plugin test
文件說耗時每次不同,所以下面括號裡的時間與文件範例不一樣。
tests/first-mod.test.ts:
(pass) /tally reports the tool calls the mod has seen [72.16ms]
1 pass
0 fail
Ran 1 test across 1 file. [0.32s]
文件說測試失敗時命令以狀態 1 結束,所以可進 CI。文件也說測試裡 session.start 不會自己執行,而 $.command.run 直接把事件送進 mod 的 command.run 事件函式;因此這個測試沒有經過 $.command.register,通過不代表 /tally 已出現在指令清單,那要看互動 session 的紀錄。
安全與界線
mod 以你的權限在行程內執行:可讀寫你帳號能碰的檔案、啟動程式、發網路請求、讀環境變數與設定檔裡的 API key、看到每個提示與工具呼叫,也能在你被詢問前核准工具呼叫。它不在沙盒裡;文件的結論是只從信得過的作者與 marketplace 安裝。
validate 的 calls 行是檢查點。文件說,事件函式要在自己的程式碼之外做任何事,例如畫面、加指令、呼叫模型、讀檔、啟動程式或發網路請求,都得呼叫 mods API,沒有別的途徑,所以 Claude Code 能在安裝前列出 mod 做什麼。first-mod 只呼叫 $.command.register 與 $.ui.invalidate,沒有檔案、行程或網路。安裝別人的 mod 前,可先對取得的目錄跑 validate,原始碼仍要讀。
文件說 --plugin-dir 載入的目錄是 protected path,請 Claude 修改時,default 與 acceptEdits 模式會逐次詢問你。分享給別人時,在 README 寫明你測試用的版本。
完成判準與小練習
完成判準:validate 的 hooks 行列出四個事件、calls 行列出兩個方法;plugin test 一個測試通過;熱重載後 spinner 文字改變有紀錄。前兩項本站在容器驗證過(2.1.289,2026-10-04),熱重載一項是文件預期,沒完成就標成待測。
下表是本課三種最常見的卡點;每一列的處理都能用 validate 或 test 的輸出核對。
| 症狀 | 檢查位置 | 處理 |
|---|---|---|
| /tally 不在指令清單 | validate 的 hooks 行有沒有 session.start | 事件名用字串字面值,重載後再看 /plugin |
| 驗證器說 is not an event | on 的第一個引數 | 對照 reference 頁的事件名改回 |
| 驗證器說 is used as a value | 有沒有把 $ 或 $.ui 存進變數 | 每次呼叫都完整寫 $.命名空間.方法 |
故障練習請保存實際輸出:把 'tool.call' 改成 'tool.calls',預期 "tool.calls" is not an event;改回後,把 $.ui.invalidate('ui.render') 改成先寫 const ui = $.ui 再呼叫,預期 $.ui is used as a value。最後改回原樣,重跑 validate 與 test。
小練習:替測試檔加第二個測試,不觸發工具呼叫就跑 /tally,文件說每個測試開始時 module 都是剛載入、模組層變數是初始值,所以預期 Claude has made 0 tool calls since this mod loaded。同一個 plugin 裡的設定檔 Hook 換到別台機器失效時,讀跨平台 Hook 排錯Claude Code|跨平台 Hooks:中文路徑、逾時與遞迴排錯把 Hook 做成可測試、可停用、可移植的工具。Hook 在作者的資料夾能跑,不代表移到中文路徑、另一個 shell 或不同作業系統也正常。本篇建立一張環境矩陣,檢查路徑、編碼、換行、逾時與重複事件,最後寫出能停止並恢復正常工作的操作手冊。閱讀全文。
回 Claude Code 教學總目錄Claude Code 完整教學目錄:從入門到自動化依平台、程度與功能找到需要的教學,從 96 篇文章與共用練習專案逐步完成操作。這個教學中心把 Claude Code 分成 96 個可以獨立閱讀的小題目,從桌面、CLI、網頁與手機開始,再學 MD 規則、常用指令、Skills、MCP 與自動化。你可以依推薦路線循序學習,也可以直接搜尋正在遇到的功能、命令或檔名。目錄依目前公開狀態顯示可閱讀文章。閱讀全文
同主題延伸閱讀
生活分享
Claude Code|Git Worktree 平行工作
隔離多個任務的檔案與分支。Git Worktree 讓同一儲存庫擁有多個工作目錄,各自使用分支與檔案。本篇會把待辦篩選與文件整理分開,確認兩個 session 不會直接改到彼此的檔案,再把其中一個成果整合回主分支。你也會知道何時可以安全清理工作目錄。
生活分享
Claude Code|雙 Worktree 實作與衝突整合
隔離兩項功能,最後完成整合與回歸。兩個 Claude 工作階段同時編輯專案,最容易出現的問題是互相改到同一份檔案,或各自測試通過、整合後卻失敗。本篇用兩個 Worktree 分別處理篩選預設值與介面文字,故意製造一次小衝突,再完成整合、驗證與清理。你不需要先啟用 Agent Teams。
生活分享
Claude Code|比較流程品質、用量與執行時間
以同一資料集比較兩種工作方法。比較兩種 Claude 工作方法時,不能只挑成功那一次,也不能只看第一個答案有多快。本篇用固定案例、原始紀錄和一致判準,比較品質、重試、等待與人工整合時間,最後寫出有樣本數與限制的報告,而不是保證某個方法一定省錢。
生活分享
Claude Code|用量、成本與效率調整
縮小任務、整理上下文與選擇模型。提升 Claude Code 效率,先減少不必要的工作,再考慮模型與設定。本篇用同一個待辦功能比較任務範圍、上下文與驗證方式,建立一份用量觀察表。你會分清楚訂閱額度、API 費用估計與實際帳務,不用單一數字判斷整個工作是否划算。
最新旅遊情報攻略

情報
2026 韓國楓葉預測:雪嶽山 10 月 20 日、首爾近郊 10 月底、內藏山與漢拏山 11 月上旬
韓國山林廳 2026 年 9 月 22 日公布的楓紅高峰預測:雪嶽山 10 月 20 日,春川、國立樹木園到首爾植物園落在 10 月 28 日到 11 月 2 日,內藏山 11 月 4 日、漢拏山 11 月 6 日,整體比最近 5 年晚約 0.8 天。整理各地楓樹與銀杏的預測日、首爾出發怎麼排,以及出發前去哪裡看即時楓況。2026 年 10 月查證。
- 季節活動
- 自然
- 觀景

攻略胡志明市
胡志明市到頭頓一日遊:白藤碼頭搭高速船、船票與班次,下船就是胡梅纜車與耶穌基督像
人在胡志明市挪一天去頭頓看海:市中心的白藤高速船碼頭搭船,航程 120 分鐘到頭頓的胡梅碼頭,平日成人 320,000 越南盾、週末 350,000,回程末班平日 15:00。下船就是胡梅纜車站,同一條路上有白宮,小山頂上是耶穌基督像。平日一天只有兩班船,整天要從末班船倒推著排。
- 交通
- 行程範例
- 海灘

攻略沖繩
沖繩不開車攻略:單軌只到浦添,美麗海水族館要坐兩個多小時的巴士,回那霸的最後一班直達車 17:22 就開走
不租車的沖繩怎麼移動:那霸市區靠沖繩都市單軌電車(ゆいレール),那霸機場站到終點てだこ浦西 19 站、17 公里、37 分鐘,一日券 1,000 日圓;美麗海水族館有那霸機場直達的高速巴士,單程 2,000 日圓起、官方時刻表上 2 小時上下,下車後還要走 10 分鐘;古宇利島要在今帰仁村役場轉車,當天來回光坐車就六個半小時;回程的最後一班直達車 17:22 就從記念公園前開走(2026 年 9 月查證)。
- 交通
- 行程範例
- 預算
資料來源
- Mods overview · 查證日期:
- Create a mod · 查證日期:
- Test a mod · 查證日期:
- Mods reference · 查證日期: