SpyBara
Go Premium

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

This page contains 92 additions and 65 deletions.

2026
Thu 1 23:59 Fri 2 16:01

使用 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 事件。'*' 符合除遙測事件之外的每個事件,遙測事件需使用其自身的名稱以及 { to: 'collector' } 篩選器。

為每個 matcher 註冊一次事件。如果您為 session.start 呼叫 on 兩次而沒有 matcher,模組將無法載入,並出現 on("session.start") is registered twice without a matcher。將您的 mod 在工作階段開始時執行的所有操作放在一個 hook 中。

對 Claude 正在執行的動作設定 hook

處理這些事件,即可在工具呼叫、提示詞或回合發生時檢視或變更它們。如需所有事件以及 hook 可傳回的內容,請參閱事件參考。

防護或變更工具呼叫

tool.call hook 會看到 Claude 即將使用的每個工具,因此可以拒絕該呼叫、變更其引數,或讓它通過。tool.call 會在 Claude Code 即將執行工具時觸發,包括 subagent 發出的呼叫以及對 MCP 工具的呼叫。e.tool 是工具的名稱,而工具的引數則是 e 的欄位,例如 Bash 的 e.command。當您呼叫 next(e) 時,Claude Code 會執行權限檢查,然後執行工具。

此 hook 會拒絕執行 force push 的 Bash 命令,並告訴 Claude 原因:

// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

當 Claude 嘗試執行 git push --force 時,命令不會執行,也不會出現權限提示,因為 hook 從未呼叫 next。Claude 會將 deny 文字當作工具的結果來讀取,因此請將其撰寫為 Claude 可據以行動的指令。其他所有 Bash 命令的執行方式都與沒有 mod 時相同。

若要在工具執行後採取動作,請 await next(e)、執行您的工作,然後傳回 next 給您的內容。此 hook 會使用 $.ui.log 記錄 Claude 變更的每個 .mdx 檔案,該函式會在逐字稿中加入一行 Claude 不會讀取的淡色文字:

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Wait for the permission check and the tool, and keep what they produced
  const result = await next(e)
  // A refused call comes back as { deny }, and a failed one has isError set
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Return the result as it came, so Claude reads what the tool returned
  return result
})

在 Claude 編輯或寫入 .mdx 檔案後,逐字稿中會出現一行淡色文字,標示該檔案名稱。對於其他類型的檔案,或是遭拒絕或失敗的呼叫,則不會記錄任何內容。Claude 對該呼叫的認知不會改變,因為 hook 傳回的是它所收到的結果。

若要變更呼叫,請將變更後的引數傳給 next。若要重試呼叫,請再次呼叫 next(e):在第一個結果上看到 isError 的 hook 可以再次執行工具,並傳回該結果。若要自行回應呼叫,請在不呼叫 next 的情況下傳回具有 result 欄位的物件,例如 { result: 'Skipped by my-mod' }。這樣做時,不會出現權限提示,工具也不會執行,因此您傳回的結果就是 Claude 對所發生事情的全部了解。

您組織的受管設定中的 hook 會在任何 mod 的 tool.call hook 之前執行,且其中任一個所做的封鎖都是最終決定。

暫停工具呼叫,直到使用者做出決定

hook 可以暫停工具呼叫,並在其繼續之前詢問使用者該怎麼做。tool.call hook 可以在呼叫 next 或傳回之前先 await,而工具呼叫會保持等待狀態直到那時。若要向使用者提出問題,請呼叫 $.ui.ask。它會在 Claude 用來詢問您的對話框中,於您的選項編號清單上方顯示您的問題,並解析為使用者所選的標籤。在您的選項之後,對話框會加入一列供輸入其他答案,以及一列 Chat about this。

此範例中的 RISKY 模式會比對 rm -r、rm -rf、git reset --hard 以及帶有 --force 的 git push,但會漏掉其他寫法,例如 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) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

當 Claude 嘗試執行如 rm -rf build 之類的命令時,會出現包含該命令的問題,而命令會等待答案:

  • 使用者選擇 Run it:hook 會呼叫 next(e),之後一般的權限檢查仍會執行
  • 使用者選擇 Refuse:命令不會執行,Claude 會讀取 deny 文字
  • 使用者輸入答案:$.ui.ask 會解析為輸入的文字。hook 會將其與 Run it 比較,因此任何其他文字都會拒絕該命令。
  • 沒有人回答:當使用者關閉問題或選擇 Chat about this 時,以及在 claude -p 執行中,$.ui.ask 會拒絕(reject),因此 catch 區塊會讓答案維持為 Refuse

請將等待保留在如 $.ui.ask 之類的 mods API 呼叫中,因為這段時間不會計入 hook 的時間限制。等待您自己的 promise 所花費的時間則會計入。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) => {
  // What the permission rules and settings hooks decided: 'allow', 'ask', or '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 可以傳回 allow、ask 或 deny,因此它也可以核准受管設定以外的 PreToolUse hook 所封鎖的呼叫。使用 hook 擴充權限列出了哪些決定的優先順序高於 mod。

改寫或補充提示詞

prompt.submit hook 會在回合開始之前看到每個提示詞,因此可以改寫文字或加入內容。e.text 是輸入的內容。

若要執行此操作 請傳回
改寫提示詞。逐字稿中的訊息會顯示新文字。 next({ ...e, text: newText })
在提示詞之後加入只有 Claude 會讀取的文字 next({ ...e, context: [...(e.context ?? []), extraText] })
阻止送出提示詞 { drop: 'the reason' }

每當提示詞提及 pull request 時,此 hook 就會為 Claude 加入目前的分支名稱:

on('prompt.submit', async ($, e, next) => {
  // Pass on a prompt that doesn't mention a pull request as it is
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // Outside a git repository the command fails, so there's no branch to add
  if (git.exitCode !== 0) return next(e)
  // Keep any context an earlier hook added, and add one more line for Claude
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

當您送出如 open a PR for this change 之類的提示詞時,您的訊息在逐字稿中看起來不變,而 Claude 還會在其後讀到如 Current branch: feature/auth 之類的一行。未提及 pull request 的提示詞會原封不動地通過,且不會執行 git。

其他事件涵蓋 Claude 讀取的其餘內容:prompt.section 用於系統提示詞的每個區段,prompt.context 用於隨第一則訊息送出的上下文,而 skill.prompt 用於 skill 的文字。來自這些 hook、且在請求之間有所變動的文字,會使提示快取失效。

追蹤回合

回合是 Claude 為回應一個提示詞所做的一切。處理 turn.start、turn.step 和 turn.complete 即可追蹤回合:

事件 觸發時機 hook 可以做什麼
turn.start 回合開始時 觀察。e.turnId 會在另外兩個事件中識別該回合。
turn.step Claude Code 即將向模型送出一個請求時。包含工具呼叫的回合會有多個請求。subagent 的請求會設定 e.agentId。 讀取每個請求的 token 使用量、使用 next({ ...e, model }) 將其送往不同的模型,或在不呼叫模型的情況下自行回應
turn.complete 回合結束時,包括使用者中斷的回合,此時 e.isAborted 為 true。e.answer 是 Claude 的最終文字,e.durationMs 是所花費的時間,e.usage 是該回合的 token 總數。subagent 的回合觸發此事件時會設定 e.agentId。 觀察,或傳回具有 text 欄位的物件(例如 { text: 'Done in 12 seconds' }),以在答案下方顯示一行文字

請將 turn.step hook 撰寫為非同步產生器(async generator),因為此事件是串流的。yield* next(e) 會在串流時轉送回應,並求值為最終結果。此 hook 會記錄每個請求中有多少是由 Claude API 從提示快取提供:

// function* makes the hook a generator, which can pass the response on piece by piece
on('turn.step', async function* ($, e, next) {
  // Send the request, forward each piece as it arrives, and keep the finished result
  const result = yield* next(e)
  // Skip a result that reports no token counts
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Return the result unchanged, so the turn continues as usual
  return result
})

Claude 的回應會像沒有 mod 時一樣串流至畫面。每個請求完成後,逐字稿中會出現一行淡色文字,列出從快取讀取的 token 數量以及寫入快取的 token 數量。包含工具呼叫的回合會有多個請求,因此會加入多行。

result.usage 保存 Claude API 為請求回報的 token 計數,以及回應的 model:input_tokens、output_tokens、cache_read_input_tokens 和 cache_creation_input_tokens。此 hook 也會針對 subagent 的請求執行,因此若您只想要主要對話,請檢查 e.agentId。

處理設定 hook 事件

設定 hook 是您在設定檔中設定的 command、HTTP、prompt 和 agent hook。每個設定 hook 事件,例如 Stop、SessionEnd 或 PostToolUse,也是一個名為 classic. 後接該設定 hook 事件名稱的事件,例如 classic.Stop。e 是設定 hook 在 stdin 上收到的 JSON,包括 transcript_path。

此 hook 使用在 Claude 完成回應時觸發的 Stop,來記錄工作階段逐字稿的儲存位置:

on('classic.Stop', async ($, e, next) => {
  // e has the same fields a Stop hook in a settings file reads from stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Pass the event on, so Stop hooks in your settings files still run
  return next(e)
})

每次 Claude 完成回應時,逐字稿中會出現一行淡色文字,列出逐字稿檔案的路徑。此 hook 傳回 next(e),因此它只觀察事件,不會改變回合結束的方式。

與其他 mod 並行執行

多個 mod 可以處理相同的事件,其中任何一個都可能失敗。如果您的 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 並執行命令。處理程式有其自己較短的時間限制。

後續步驟