使用 mod 回應事件
從 mod 處理 Claude Code 事件:觀察、重寫或回答工具呼叫、提示和回合,篩選 hook 處理哪些事件,並為其他 mod 進行規劃。
hook 是一個事件處理程式:一個函式,Claude Code 在命名事件發生時執行。Claude Code 在每個即將採取行動的地方觸發事件,例如當它執行工具、提交提示、向模型發送請求或啟動或結束工作階段時。您的 hook 在 Claude Code 採取行動之前執行,因此它可以觀察事件、重寫事件或代替 Claude Code 回答事件。您使用 on(eventName, handler) 註冊 hook。
在開始之前,請先建立您的第一個 mod。對於每個事件及其確切欄位,請參閱參考資料或閱讀您的建置類型。
hook 如何處理事件
hook 位於事件和 Claude Code 對其採取的行動之間,因此它可以觀察事件、重寫事件或自己回答事件。它接收三個引數:mods API 作為 $、事件作為 e,以及下一個處理程式作為 next。事件的處理程式形成中介軟體鏈。next(e) 呼叫下一個處理程式,這是另一個 mod 的 hook 或在鏈的末端是 Claude Code 自己的行為,它解析為結果。您的 hook 對 next 的處理決定了它執行以下三項中的哪一項。
觀察事件
若要觀察事件而不改變它,請執行您的工作並傳回 next(e)。此 hook 記錄 Claude 即將使用的每個工具:
on('tool.call', async ($, e, next) => {
// 在工具執行之前執行
$.ui.log('Claude is about to use ' + e.tool)
// 將事件原封不動地傳遞
return next(e)
})
在每個工具執行之前,會在文字記錄中出現一行暗淡的行,例如 ● my-mod: Claude is about to use Bash,其中 my-mod 是您的外掛程式名稱。工具的執行方式與沒有 mod 時相同。
若要在事件後採取行動,請 await next(e)、執行您的工作,然後傳回結果。此 hook 在每個工具執行後記錄它:
on('tool.call', async ($, e, next) => {
// 讓工具執行,並等待其結果
const result = await next(e)
// 在工具執行後執行
$.ui.log(e.tool + ' finished')
// 將結果原封不動地傳回
return result
})
該行現在出現在每個工具完成後。Claude 讀取相同的結果,因為 hook 傳回 next(e) 解析的內容。
重寫事件
若要變更 Claude Code 作用的內容,例如提示的文字,請使用修改後的事件副本呼叫 next。事件本身是不可變的:它在每個深度都被凍結,分配給欄位會擲回。此 hook 在傳送前修剪每個提示:
on('prompt.submit', async ($, e, next) => {
// 傳遞事件的副本,其文字已變更
return next({ ...e, text: e.text.trim() })
})
稍後的處理程式和 Claude Code 會收到修剪後的提示,永遠看不到原始提示。您也可以變更結果:await next(e),然後傳回結果的副本,其中欄位已替換。
回答事件
若要自己處理事件,請傳回結果而不呼叫 next。這會短路鏈,因此稍後的 mod 和 Claude Code 自己的行為不會執行。此 hook 拒絕每個 Bash 命令:
on('tool.call', { tool: 'Bash' }, async () => {
// 沒有呼叫 next,所以命令永遠不會執行
return { deny: 'Bash is turned off in this project. Use the file tools.' }
})
當 Claude 嘗試 Bash 命令時,命令不會執行,Claude 會將 deny 文字讀取為工具的結果。每個事件都有自己的結果形狀,事件參考資料會列出。
篩選 hook 處理哪些事件
若要僅針對某些事件執行 hook,請將篩選器作為第二個引數傳遞給 on。Claude Code 將篩選器稱為 matcher。它是一個物件,其欄位與事件的欄位進行比較,只有當每個欄位都符合時,hook 才會執行。欄位可以是值、允許值的陣列或正規表達式。
此範例中的每一行都為較窄的工具呼叫集合註冊相同的函式 hook:
// 字串符合一個值:僅限 Bash 呼叫
on('tool.call', { tool: 'Bash' }, hook)
// 陣列符合其中任何值:Edit 呼叫和 Write 呼叫
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// 正規表達式按模式符合:一個 MCP 伺服器的每個工具
on('tool.call', { tool: /^mcp__github__/ }, hook)
hook 針對 Bash、Edit 或 Write 呼叫執行一次,並針對名稱以 mcp__github__ 開頭的工具呼叫執行一次。對任何其他工具(例如 Read)的呼叫不符合這三個中的任何一個,因此 hook 不會針對它執行。
事件名稱可以是萬用字元。'classic.*' 符合每個設定 hook 事件。'*' 符合除遙測事件之外的每個事件,您可以按名稱或作為 'telemetry.*' 進行 hook。
為每個 matcher 註冊一次事件。如果您為 session.start 呼叫 on 兩次而沒有 matcher,模組將無法載入,並出現 on("session.start") is registered twice without a matcher。將您的 mod 在工作階段開始時執行的所有操作放在一個 hook 中。
Hook Claude 正在執行的操作
Hook 這些事件以查看或變更工具呼叫、提示或回合。對於每個事件以及 hook 可以傳回的內容,請參閱事件參考資料。
保護或變更工具呼叫
tool.call hook 會看到 Claude 即將使用的每個工具,因此它可以拒絕呼叫、變更其引數或讓它通過。tool.call 在 Claude Code 即將執行工具時觸發,包括子代理程式進行的呼叫和對 MCP 工具的呼叫。e.tool 是工具的名稱,工具的引數是 e 的欄位,例如 Bash 的 e.command。當您呼叫 next(e) 時,Claude Code 執行權限檢查,然後執行工具。
此 hook 拒絕強制推送的 Bash 命令,並告訴 Claude 原因:
// matcher 將 hook 限制為 Bash 呼叫,因此 e.command 是 shell 命令
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/git push .*--force/.test(e.command)) {
// 傳回而不呼叫 next 會回答事件,所以命令永遠不會執行
return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
}
// 每個其他命令都會進行權限檢查,然後進行 Bash
return next(e)
})
當 Claude 嘗試 git push --force 時,命令不會執行,也不會出現權限提示,因為 hook 永遠不會呼叫 next。Claude 將 deny 文字讀取為工具的結果,因此將其寫成 Claude 可以採取行動的指示。每個其他 Bash 命令的執行方式與沒有 mod 時相同。
若要在工具執行後採取行動,請 await next(e)、執行您的工作,然後傳回 next 給您的內容。此 hook 記錄 Claude 變更的每個 .mdx 檔案,使用 $.ui.log,它會在文字記錄中新增一行暗淡的行,Claude 不會讀取:
on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
// 等待權限檢查和工具,並保留它們產生的內容
const result = await next(e)
// 被拒絕的呼叫會以 { deny } 的形式返回,失敗的呼叫會設定 isError
const changed = !result.deny && !result.isError
if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
// 按原樣傳回結果,以便 Claude 讀取工具傳回的內容
return result
})
Claude 編輯或寫入 .mdx 檔案後,文字記錄中的暗淡行會命名該檔案。對於另一種檔案或被拒絕或失敗的呼叫,不會記錄任何內容。Claude 對呼叫的檢視不會改變,因為 hook 傳回它收到的結果。
若要變更呼叫,請將變更的引數傳遞給 next。若要重試呼叫,請再次呼叫 next(e):看到第一個結果上的 isError 的 hook 可以第二次執行工具並傳回該結果。若要自己回答呼叫,請傳回具有 result 欄位的物件,例如 { result: 'Skipped by my-mod' },而不呼叫 next。當您這樣做時,不會出現權限提示,工具不會執行,因此您傳回的結果是 Claude 了解發生情況的全部內容。
您組織的受管設定中的 hook 在任何 mod 的 tool.call hook 之前執行,其中一個的區塊是最終的。
保留工具呼叫直到使用者決定
hook 可以暫停工具呼叫並在繼續之前詢問使用者該怎麼做。tool.call hook 可以在呼叫 next 或傳回之前 await,工具呼叫會保持待處理狀態直到那時。若要向使用者提出問題,請呼叫 $.ui.ask。它在 Claude 用來詢問您的對話框中的編號選項清單上方顯示您的問題,並解析為使用者選擇的標籤。在您的選項之後,對話框會新增一行用於輸入不同的答案和一個聊天此項目行。
此範例中的 RISKY 模式符合 rm -r、rm -rf、git reset --hard 和 git push 搭配 --force,並且會遺漏其他拼寫,例如 git push -f。此模組在執行符合模式的 Bash 命令之前詢問:
const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/
export function register(on) {
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
// 讓每個其他命令通過而不提出問題
if (!RISKY.test(e.command)) return next(e)
// 從安全答案開始,因此沒有人回答的問題會拒絕命令
let answer = 'Refuse'
try {
// 工具呼叫在此等待,直到使用者選擇兩個標籤之一
answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
} catch {
// 使用者關閉了問題,或這是一個沒有人可以詢問的 claude -p 執行
}
if (answer !== 'Run it') {
// 回答而不呼叫 next,所以命令不會執行
return { deny: 'The user declined this command. Ask before trying a different approach.' }
}
return next(e)
})
}
當 Claude 嘗試命令(例如 rm -rf build)時,問題會出現並帶有命令,命令會等待答案:
- 使用者選擇執行它:hook 呼叫
next(e),通常的權限檢查仍在之後執行 - 使用者選擇拒絕:命令不會執行,Claude 讀取
deny文字 - 使用者輸入答案:
$.ui.ask解析為輸入的文字。hook 將其與Run it進行比較,因此任何其他文字都會拒絕命令。 - 沒有人回答:當使用者關閉問題或選擇聊天此項目時,
$.ui.ask會拒絕,在claude -p執行中也是如此,因此catch區塊將答案保留在Refuse
將等待保留在 mods API 呼叫(例如 $.ui.ask)內,因為該時間不計入 hook 的10 秒時間限制。花費在等待您自己的承諾上的時間確實計入。Claude Code 會跳過超時的 hook,因此保留的命令會執行。
重寫或新增至提示
prompt.submit hook 在回合開始之前看到每個提示,因此它可以重寫文字或新增至文字。e.text 是輸入的內容。
| 若要執行此操作 | 傳回此項 |
|---|---|
| 重寫提示。文字記錄中的訊息顯示新文字。 | next({ ...e, text: newText }) |
| 新增僅 Claude 讀取的文字,在提示之後 | next({ ...e, context: [...(e.context ?? []), extraText] }) |
| 停止傳送提示 | { drop: 'the reason' } |
此 hook 在提示提及提取要求時為 Claude 新增目前分支名稱:
on('prompt.submit', async ($, e, next) => {
// 將不提及提取要求的提示按原樣傳遞
if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
const git = await $.process.run(['git', 'branch', '--show-current'])
// 在 git 存放庫外,命令失敗,因此沒有分支可新增
if (git.exitCode !== 0) return next(e)
// 保留較早的 hook 新增的任何內容,並為 Claude 新增一行
return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})
當您傳送提示(例如 open a PR for this change)時,您的訊息在文字記錄中看起來相同,Claude 也會在其後讀取一行,例如 Current branch: feature/auth。不提及提取要求的提示會原封不動地通過,git 不會執行。
其他事件涵蓋 Claude 讀取的其餘內容:prompt.section 用於系統提示的每個部分,prompt.context 用於與第一條訊息一起傳送的內容,以及 skill.prompt 用於技能的文字。來自這些 hook 的文字在請求之間變更時會使提示快取失效。
追蹤回合
回合是 Claude 為回應一個提示而執行的所有操作。Hook turn.start、turn.step 和 turn.complete 以追蹤一個:
| 事件 | 何時觸發 | hook 可以執行的操作 |
|---|---|---|
turn.start |
回合開始 | 觀察。e.turnId 在其他兩個事件中識別回合。 |
turn.step |
Claude Code 即將向模型發送一個請求。具有工具呼叫的回合有多個。e.agentId 針對子代理程式的請求進行設定。 |
讀取每個請求的令牌使用情況,使用 next({ ...e, model }) 將其傳送到不同的模型,或在不呼叫模型的情況下回答 |
turn.complete |
回合結束,包括使用者中斷的回合,其中 e.isAborted 為 true。e.answer 是 Claude 的最終文字,e.durationMs 是花費的時間,e.usage 是回合的令牌總計。子代理程式的回合會以 e.agentId 設定的方式觸發它。 |
觀察,或傳回具有 text 欄位的物件,例如 { text: 'Done in 12 seconds' },以在答案下方顯示一行 |
將 turn.step hook 寫成非同步產生器,因為事件會串流。yield* next(e) 在串流時轉發回應並評估為完成的結果。此 hook 記錄每個請求中有多少來自提示快取的 Claude API:
// function* 使 hook 成為產生器,可以逐段傳遞回應
on('turn.step', async function* ($, e, next) {
// 傳送請求,在每個片段到達時轉發它,並保留完成的結果
const result = yield* next(e)
// 跳過不報告令牌計數的結果
if (result.usage) {
$.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
}
// 傳回結果不變,以便回合照常繼續
return result
})
Claude 的回應會串流到螢幕,就像沒有 mod 時一樣。每個請求完成後,文字記錄中的暗淡行會給出從快取讀取的令牌數和寫入的令牌數。具有工具呼叫的回合有多個請求,因此它會新增多行。
result.usage 保留 Claude API 為請求報告的四個令牌計數,加上回答的 model:input_tokens、output_tokens、cache_read_input_tokens 和 cache_creation_input_tokens。hook 也針對子代理程式的請求執行,因此當您只想要主要對話時,請檢查 e.agentId。
Hook 設定 hook 事件
設定 hook 是您在設定檔中設定的命令、HTTP、提示和代理程式 hook。每個設定 hook 事件(例如 Stop、SessionEnd 或 PostToolUse)也是一個名為 classic. 後跟設定 hook 事件名稱的事件,例如 classic.Stop。e 是設定 hook 在 stdin 上接收的 JSON,包括 transcript_path。
此 hook 使用 Stop(在 Claude 完成回應時觸發)來記錄工作階段的文字記錄的儲存位置:
on('classic.Stop', async ($, e, next) => {
// e 具有設定檔中的 Stop hook 從 stdin 讀取的相同欄位
$.ui.log('Transcript saved at ' + e.transcript_path)
// 傳遞事件,以便設定檔中的 Stop hook 仍然執行
return next(e)
})
每次 Claude 完成回應時,文字記錄中的暗淡行會給出文字記錄檔案的路徑。hook 傳回 next(e),因此它觀察事件並不改變回合結束的方式。
與其他 mod 並行執行
多個 mod 可以 hook 相同的事件,其中任何一個都可能失敗。如果您的 mod 阻止工具呼叫,請檢查其在鏈中的位置以及其 hook 失敗時會發生什麼。
mod 執行的順序
相同事件上的 hook 形成一個中介軟體鏈。每個 mod 的 next 呼叫以下 mod 的 hook,最後一個 next 到達 Claude Code 自己的行為。第一個 mod 是最外層的:它在其他 mod 之前看到事件,在它們之後看到結果,並決定其他 mod 是否執行。稍後的 mod 無法阻止較早的 mod 看到事件。
Claude Code 按每個 mod 的來源順序排列鏈:
- 內建保護
sec-default@builtin,一個內建於 Claude Code 的 mod,/plugin列為cc-plugin-sec-default,其中它載入,您的組織在prependPlugins中列出的 mod,然後是任何其他計為您的組織的 mod,且不在appendPlugins中 - 您安裝的 mod
- 您的組織在
appendPlugins中列出的 mod - 內建於 Claude Code 的其他 mod
在您安裝的 mod 中,mod 在其清單中的 dependencies 下列出的 mod 之前執行。在一個模組中,hook 按 register 呼叫 on 的順序執行。
設定 hook 在順序中執行的位置
在設定檔中設定的 PreToolUse hook 也在工具呼叫期間執行,在 mod 鏈中的固定點:
- 來自受管設定的
PreToolUsehook:在第一個 mod 的tool.callhook 之前執行,其中一個的區塊是最終的,因此沒有 mod 看到呼叫。 - 來自每個其他設定檔和外掛程式
hooks/hooks.json的PreToolUsehook:在最後一個 mod 呼叫next後執行,作為 Claude Code 自己行為的一部分。回答tool.call而不呼叫next的 mod 會阻止它們執行,呼叫next的 mod 會在它傳回的結果中看到它們的決定。
tool.check 是 Claude Code 決定是否允許工具呼叫執行的事件。它在這些 hook 和權限規則決定後觸發,next(e) 解析為它們的決定。tool.check 上的 hook 可以傳回不同的決定,例如 { decision: 'allow' },因此它可以批准第二組中的 hook 阻止的呼叫。使用 hook 擴展權限列出哪些決定優先於 mod。
處理失敗的 hook
失敗的 hook 不會破壞工作階段,您可以決定接下來會發生什麼。當沒有 .catch 處理程式的 hook 擲回、超時或傳回錯誤形狀的結果時,接下來會發生什麼取決於它是否已呼叫 next:
- 它在呼叫
next之前失敗:Claude Code 跳過它,下一個處理程式代替執行 - 它在
next解析後失敗:該結果成立,沒有任何東西執行第二次
一行命名 mod、事件和原因,例如 my-mod: tool.call hook skipped: threw Error: boom。您讀取它的位置取決於工作階段,如找出 mod 為什麼不執行任何操作所列。其繪圖不驗證的 ui.render hook 的報告方式不同,如從元素建立樹所述。
若要使阻止呼叫的 hook 失敗關閉,請新增 .catch 錯誤處理程式以代替回答。此處,guard 是您的 hook 函式:
// on 傳回註冊,.catch 將處理程式附加到該 hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
// next.error.kind 是 'throw' 或 'timeout',說明 guard 如何失敗
return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})
當 guard 有效時,處理程式永遠不會執行。當 guard 在 Bash 呼叫上擲回或超時時,Claude Code 使用相同的事件呼叫處理程式。處理程式傳回 { deny },因此命令不會執行,Claude 讀取末尾帶有 throw 或 timeout 的文字。沒有處理程式,Claude Code 會跳過 guard 並執行命令。處理程式有一秒來回答。
後續步驟
- 使用 mods API:新增命令和工具、呼叫模型,以及在計時器上執行工作
- 在介面中繪製:在窗格或提示上方顯示您的 hook 收集的內容
- 測試 mod:從測試中引發任何這些事件
- Mods 參考資料:每個事件、每個 mods API 方法和限制