Skip to content

Hooks: cadena de ejecución de ganchos del usuario

源码版本v4.0.10

Responsabilidades

El sistema de hooks de Cline permite al usuario insertar sus propios shell scripts en varios puntos del ciclo de vida del agent loop principal (antes de enviar el prompt, antes/después de usar una herramienta, al iniciar/finalizar la tarea, al emitir notificaciones, antes de la compresión). Los scripts pueden leer JSON estructurado por stdin y devolver JSON por stdout para controlar el comportamiento de Cline —lo más relevante es que cancel: true bloquea la acción inminente, y contextModification permite añadir contexto extra a la conversación. El mecanismo completo se divide en tres capas: HookExecutor como entrada que dispatcha (executeHook:58), HookFactory + HookRunner que descubren y ejecutan el script concreto (HookRunner exec:289), y HookProcess que gestiona el subproceso subyacente (HookProcess:89).

Atraviesa todo el ciclo de vida de Task. PreToolUse / PostToolUse los llama ToolExecutor antes y después de ejecutar la herramienta (PreToolUse executeHook:73); UserPromptSubmit / TaskStart / TaskResume / TaskCancel los invoca el Task principal en los puntos de ciclo de vida correspondientes (UserPromptSubmit:1214); Notification lo emite de forma unificada el módulo NotificationHook (emitNotificationHook:50). La semántica «interrumpible» del hook solo aplica en puntos cancelables; hooks fire-and-forget como Notification ignoran el cancel devuelto.

Motivación de diseño

  • 9 hooks estándar: PreToolUse / PostToolUse / UserPromptSubmit / TaskStart / TaskResume / TaskCancel / TaskComplete / Notification / PreCompact (Hooks interface:102), cubren todos los puntos clave del ciclo de vida.
  • JSON in / JSON out: el stdin que recibe el hook es un JSON con metadatos clineVersion / hookName / timestamp / workspaceRoots / userId (completeParams:198), la salida también es JSON.
  • Tres campos de retorno: cancel interrumpe la acción, contextModification añade contexto, errorMessage adjunta una explicación de error (HookExecutionResult:33).
  • Flag cancellable: el llamador decide si el hook puede cancelar la acción; Notification pasa isCancellable: false (not cancellable:61), PreToolUse pasa true.
  • Aislamiento de procesos + registro: HookProcessRegistry rastrea todos los procesos hook activos, terminateAll los mata de forma unificada al cancelar la tarea (HookProcessRegistry:17).
  • Lanzador multiplataforma: getHookLaunchConfig distingue PowerShell en Windows de shebang en Unix (getHookLaunchConfig:44), para que el usuario pueda correr .sh / .ps1 / .js.
  • Echo en streaming de stdout: HookProcess emite stdout/stderr por líneas, streamCallback añade el prefijo [source stream path] y lo reenvía a la UI (streamCallback:124), de modo que incluso hooks largos muestran progreso.
  • Fallback de extracción de JSON: cuando un hook imprime logs de depuración mezclados antes/después del JSON, se escanea de derecha a izquierda emparejando llaves para localizar el último objeto JSON completo (JSON extraction:379).

Archivos clave

Flujo de datos

Tomando PreToolUse como ejemplo, esta es la cadena «interrumpible» más completa. ToolExecutor, antes de ejecutar la herramienta, invoca ToolHookUtils.executePreToolUseHook:

typescript
// 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")
}

Esto está cerca de PreToolUse caller:73. Al entrar en executeHook primero comprueba hooksEnabled (early return:72), si está apagado devuelve directamente. Luego HookFactory.hasHook comprueba si hay script configurado para ese nombre de hook (hasHook:80), si no hay configuración también devuelve. Tras configurarse, crea un HookProcess y hace spawn del subproceso:

typescript
// 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()

Esto está cerca de spawn child:176. Al salir el subproceso, HookRunner.exec toma stdout y lo pasa a parseJsonOutput (parseJsonOutput:349); si hay JSON válido se sigue ese camino (incluso si exitCode no es 0, el JSON tiene prioridad) (JSON priority:454), si no, se juzga por exitCode. cancel: true se propaga por HookExecutor.executeHook hasta arriba; ToolHookUtils lo recibe, lanza PreToolUseHookCancellationError y llama a cancelTask.

Límites y fallos

  • El cancel de PreToolUse aborta toda la tarea: no solo salta esa herramienta, sino que llama a cancelTask para detener todo el agent loop (cancelTask on cancel:100).
  • El cancel de PostToolUse no aborta la tarea: runPostToolUseHook al recibir cancel solo hace say error y devuelve true; la herramienta ya se ejecutó y no se puede revertir (post cancel soft:494).
  • El hook Notification ignora cancel y contextModification: este hook es una notificación unidireccional, sus campos de salida se ignoran explícitamente (ignore unsupported output:70).
  • abort signal rechaza al instante: HookProcess escucha abortSignal; al abortar mata el subproceso y rechaza sin esperar al close (abortHandler:139).
  • Timeout con ruta de script en el error: el mensaje de timeout incluye this.scriptPath y los milisegundos de timeout, para que el usuario ubique qué hook se colgó (timeout error:192).
  • Salida limitada a 1 MB: MAX_HOOK_OUTPUT_SIZE corta salidas infinitas; al superarlo emite un aviso de truncamiento (output truncation:311).
  • Truncado de contextModification: MAX_CONTEXT_MODIFICATION_SIZE evita que un hook inyecte contexto desproporcionado; al superar el límite se trunca y avisa (context truncation:363).
  • PreToolUse se salta attempt_completion: attempt_completion es una herramienta de cierre, no pasa por la阻断 de PreToolUse (skip attempt_completion:30).
  • Los hooks NoOp devuelven valores por defecto: un hook sin script devuelve cancel: false / contextModification: "" / errorMessage: "", el executor lo trata como no-op normal (NoOp runner:238).
  • Extracción de JSON escaneando de derecha a izquierda: si el hook imprime logs de depuración antes del JSON y el primer JSON.parse falla, parseJsonOutput empareja llaves para encontrar el último objeto completo (brace scan:385), un fallback crucial.

Resumen

El sistema de hooks es el punto de extensión que Cline deja al usuario para «insertar scripts en los puntos clave del agent loop». Para ver en qué momentos el Task principal dispara hooks, lee agent-loop/task-class; para ver cómo el ejecutor de herramientas encadena PreToolUse / PostToolUse, lee cap-tools; para ver cómo StateManager proporciona a los hooks metadatos como workspaceRoots, lee state-manager.

Véase la documentación oficial: Cline 文档 · README