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。