SpyBara
Go Premium

plugins/mods/events.md 2026-10-01 23:59 UTC to 2026-10-02 03:00 UTC

This page contains 28 additions and 1 deletion.

2026
Thu 1 23:59 Fri 2 03:59

使用 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,因此保留的命令會執行。

在詢問使用者之前核准或拒絕工具呼叫

若要決定工具呼叫是否可以執行,請處理 tool.check,也就是 Claude Code 做出該決定的事件。它在權限規則和設定 hook 做出決定後觸發,而 next(e) 會解析為它們的決定:allow、ask 或 deny。您的 hook 會傳回該決定或另一個決定。e.input 保存工具的引數,例如 Bash 的 command。

對於固定的命令或路徑,請使用權限規則,例如 Bash(npm test),這不需要任何程式碼。當決定取決於當下的實際狀況(例如目前的 Git 分支或另一個 hook 記錄的值)時,請處理 tool.check。

此 hook 在目前分支為 main 時拒絕 git push:

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // 權限規則和設定 hook 的決定:'allow'、'ask' 或 'deny'
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

在 main 上,即使有規則允許 git push,hook 也會傳回 deny。在其他分支上以及對於其他命令,呼叫會得到與沒有 mod 時相同的決定。

hook 比對的是命令的文字,因此請將其視為給 Claude 的提醒。若要為所有人封鎖推送到 main,請在您的 Git 主機上保護該分支。

hook 可以傳回三種決定中的任何一種,因此它也可以核准受管設定之外的 PreToolUse hook 所封鎖的呼叫。使用 hook 擴充權限列出哪些決定的效力優先於 mod。

重寫或新增至提示

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 的來源順序排列鏈:

  1. 內建保護 sec-default@builtin,一個內建於 Claude Code 的 mod,/plugin 列為 cc-plugin-sec-default,其中它載入,您的組織在 prependPlugins 中列出的 mod,然後是任何其他計為您的組織的 mod,且不在 appendPlugins 中
  2. 您安裝的 mod
  3. 您的組織在 appendPlugins 中列出的 mod
  4. 內建於 Claude Code 的其他 mod

在您安裝的 mod 中,mod 在其清單中的 dependencies 下列出的 mod 之前執行。在一個模組中,hook 按 register 呼叫 on 的順序執行。

設定 hook 在順序中執行的位置

在設定檔中設定的 PreToolUse hook 也在工具呼叫期間執行,在 mod 鏈中的固定點:

  • 來自受管設定的 PreToolUse hook:在第一個 mod 的 tool.call hook 之前執行,其中一個的區塊是最終的,因此沒有 mod 看到呼叫。
  • 來自每個其他設定檔和外掛程式 hooks/hooks.json 的 PreToolUse hook:在最後一個 mod 呼叫 next 後執行,作為 Claude Code 自己行為的一部分。回答 tool.call 而不呼叫 next 的 mod 會阻止它們執行,呼叫 next 的 mod 會在它傳回的結果中看到它們的決定。

tool.check 在這些 hook 和權限規則決定後觸發,因此其上的 hook 可以批准第二組中的 hook 所阻止的呼叫。

處理失敗的 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 並執行命令。處理程式有一秒來回答。

後續步驟