Hooks:使用者鉤子執行鏈
職責
Cline 的 hooks 系統讓使用者在主 agent loop 的若干生命點(提交 prompt 前、工具用前、工具用後、任務開始/結束、通知發出、壓縮前)插入自己的 shell 腳本。腳本能讀 stdin 拿到結構化 JSON 輸入,能透過 stdout 回傳 JSON 控制 Cline 行為——最關鍵的是 cancel: true 可以阻止即將發生的動作,contextModification 能往對話裡追加額外上下文。整套機制分三層:HookExecutor 作為入口分發 (executeHook:58),HookFactory + HookRunner 負責具體腳本的發現與執行 (HookRunner exec:289),HookProcess 管理底層子程序 (HookProcess:89)。
它橫跨整個 Task 生命週期。PreToolUse / PostToolUse 由 ToolExecutor 在工具執行前後調 (PreToolUse executeHook:73);UserPromptSubmit / TaskStart / TaskResume / TaskCancel 由主 Task 在對應生命點調 (UserPromptSubmit:1214);Notification 由 NotificationHook 模組統一發出 (emitNotificationHook:50)。hook 的「可阻斷」語義只在可取消的點生效,Notification 這種 fire-and-forget 的 hook 即便回傳 cancel 也會被忽略。
設計動機
- 9 種標準 hook:
PreToolUse/PostToolUse/UserPromptSubmit/TaskStart/TaskResume/TaskCancel/TaskComplete/Notification/PreCompact(Hooks interface:102),覆蓋所有關鍵生命點。 - JSON in / JSON out:hook 腳本收到的 stdin 是帶
clineVersion/hookName/timestamp/workspaceRoots/userId等元資料的 JSON (completeParams:198),輸出也是 JSON。 - 三種回傳欄位:
cancel阻斷動作、contextModification追加上下文、errorMessage附加錯誤說明 (HookExecutionResult:33)。 - cancellable 標誌位:呼叫方決定該 hook 能不能取消動作,
Notification傳isCancellable: false(not cancellable:61),PreToolUse 傳 true。 - 程序隔離 + 註冊表:
HookProcessRegistry追蹤所有活動 hook 程序,terminateAll在 task 取消時統一殺 (HookProcessRegistry:17)。 - 跨平台啟動器:
getHookLaunchConfig區分 Windows PowerShell 與 Unix shebang (getHookLaunchConfig:44),讓使用者用.sh/.ps1/.js都能跑。 - stdout 流式回顯:HookProcess 把 stdout/stderr 按行 emit,
streamCallback加[source stream path]前綴後回顯給 UI (streamCallback:124),長 hook 也能看到進度。 - JSON 提取兜底:hook 腳本在 JSON 前後混列印除錯日誌時,從後往前掃括號配對找出最後一個完整 JSON 物件 (
JSON extraction:379)。
關鍵檔案
Hooks interface:102— 9 種 hook 名 + 每種的輸入資料類型。HookRunner:165— 抽象 runner,run方法 stateless 可重用。completeParams:198— 給 hook 輸入補 clineVersion / hookName / timestamp / workspaceRoots / userId。ConcreteHookRunner exec:289— 真正起 HookProcess 執行腳本的核心方法。new HookProcess:324— 建 hook 子程序,綁定 abort signal 與 streamCallback。honor JSON regardless of exit code:454— 有合法 JSON 就優先於 exitCode。executeHook:58— 入口,統一錯誤處理、UI 狀態、cancel 語義。reorderHookAndToolMessages:109— PreToolUse 時把 hook UI 排到工具 UI 上面。cancel handling:166— 收到cancel: true時把 hook 狀態置 cancelled 並回傳。HookProcess:89— 繼承 EventEmitter,跑子程序並按行 emit。HookProcess.run:123— 真正 spawn 子程序、綁 stdin/stdout/stderr/exit。timeout:187— 超時 SIGTERM,錯誤訊息帶腳本路徑。HookProcessRegistry:17— 靜態註冊表,task 取消時批次 terminate。PreToolUseHookCancellationError:5— 專門例外類別,讓上層 catch 能區分 hook 取消和普通失敗。emitNotificationHook:50— Notification hook 的 fire-and-forget 包裝。PreToolUse caller:73— 工具執行前的 hook 呼叫,cancel 即拋例外並 cancelTask。PostToolUse caller:471— 工具執行後的 hook 呼叫,cancel 僅中斷後續。
資料流
以 PreToolUse 為例,這是最完整的「可阻斷」鏈。ToolExecutor 在真正執行工具前先調 ToolHookUtils.executePreToolUseHook:
// apps/vscode/src/core/task/tools/utils/ToolHookUtils.ts
const { executeHook } = await import("@core/hooks/hook-executor")
const pendingToolInfo: any = { tool: block.name }
if (block.params.path) pendingToolInfo.path = block.params.path
if (block.params.command) pendingToolInfo.command = block.params.command
// ...
const preToolResult = await executeHook({
hookName: "PreToolUse",
hookInput: {
preToolUse: { toolName: block.name, parameters: block.params },
},
isCancellable: true,
say: config.callbacks.say,
setActiveHookExecution: config.callbacks.setActiveHookExecution,
clearActiveHookExecution: config.callbacks.clearActiveHookExecution,
messageStateHandler: config.messageState,
taskId: config.taskId,
hooksEnabled,
model: getHookModelContext(config.api, config.services.stateManager),
toolName: block.name,
pendingToolInfo,
})
if (preToolResult.cancel === true) {
await config.callbacks.clearActiveHookExecution()
await config.callbacks.cancelTask()
throw new PreToolUseHookCancellationError(preToolResult.errorMessage || "PreToolUse hook requested cancellation")
}
if (preToolResult.contextModification) {
ToolHookUtils.addHookContextToConversation(config, preToolResult.contextModification, "PreToolUse")
}這段在 PreToolUse caller:73 附近。executeHook 進去後先看 hooksEnabled (early return:72),關了直接回傳。然後 HookFactory.hasHook 查是否配了這個 hook 名的腳本 (hasHook:80),沒配也直接回傳。配上後建 HookProcess 並 spawn 子程序:
// apps/vscode/src/core/hooks/HookProcess.ts
this.childProcess = spawn(launchConfig.command, launchConfig.args, {
stdio: ["pipe", "pipe", "pipe"],
shell: launchConfig.shell,
detached: launchConfig.detached,
cwd: this.cwd,
windowsHide: true,
})
this.timeoutHandle = setTimeout(() => {
if (this.childProcess && !this.isCompleted) {
this.childProcess.kill("SIGTERM")
reject(new Error(`Hook execution timed out after ${this.timeoutMs}ms.`))
}
}, this.timeoutMs)
this.childProcess.stdout?.on("data", (data) => {
this.stdoutBuffer += output
this.handleOutput(output, didEmitEmptyLine, "stdout")
})
// ...
this.childProcess.on("close", (code, signal) => {
this.exitCode = code
// resolve / reject
})
this.childProcess.stdin?.write(inputJson)
this.childProcess.stdin?.end()這段在 spawn child:176 附近。子程序退出後,HookRunner.exec 拿 stdout 去 parseJsonOutput (parseJsonOutput:349),有合法 JSON 就按 JSON 走(即便 exitCode 非 0 也優先)(JSON priority:454),沒有就按 exitCode 判斷。cancel: true 沿著 HookExecutor.executeHook 一路回傳,ToolHookUtils 拿到後拋 PreToolUseHookCancellationError 並調 cancelTask。
邊界與失敗
- PreToolUse cancel 直接 abort 整個 task:不只是跳過這個工具,而是調
cancelTask把整條 agent loop 都停掉 (cancelTask on cancel:100)。 - PostToolUse cancel 不 abort task:
runPostToolUseHook收到 cancel 只 say error 回傳 true,工具已執行完無法回滾 (post cancel soft:494)。 - Notification hook 忽略 cancel 與 contextModification:這種 hook 是單向通知,輸出欄位被顯式忽略 (
ignore unsupported output:70)。 - abort signal 立即 reject:
HookProcess監聽 abortSignal,一旦 abort 立即 kill 子程序並 reject,不等 close (abortHandler:139)。 - 超時按腳本路徑報錯:timeout 錯誤訊息帶
this.scriptPath和超時毫秒數,方便使用者定位哪個 hook 卡死 (timeout error:192)。 - 輸出 1MB 上限:
MAX_HOOK_OUTPUT_SIZE攔住無限輸出,超了 emit 一條 truncation 提示 (output truncation:311)。 - contextModification 截斷:
MAX_CONTEXT_MODIFICATION_SIZE防止 hook 注入超大上下文,超了截斷加提示 (context truncation:363)。 - PreToolUse 跳過 attempt_completion:
attempt_completion是收尾工具,不走 PreToolUse 阻斷 (skip attempt_completion:30)。 - NoOp hook 回傳 proto defaults:沒找到腳本的 hook 回傳
cancel: false / contextModification: "" / errorMessage: "",executor 把這視為正常 no-op (NoOp runner:238)。 - JSON 提取從右往左掃:hook 腳本前面打除錯日誌導致首次 JSON.parse 失敗時,
parseJsonOutput用括號配對找最後一個完整物件 (brace scan:385),兜底很關鍵。
小結
Hooks 系統是 Cline 給使用者留的「在 agent loop 關鍵點插腳本」的擴充位。想看主 Task 在哪些時刻觸發 hook 可以讀 agent-loop/task-class;想看工具執行器如何把 PreToolUse / PostToolUse 串起來可以讀 cap-tools;想看 StateManager 如何為 hook 提供 workspaceRoots 等元資料可以讀 state-manager。