Hooks: cadena de ejecución de ganchos del usuario
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:
cancelinterrumpe la acción,contextModificationañade contexto,errorMessageadjunta una explicación de error (HookExecutionResult:33). - Flag cancellable: el llamador decide si el hook puede cancelar la acción;
NotificationpasaisCancellable: false(not cancellable:61), PreToolUse pasa true. - Aislamiento de procesos + registro:
HookProcessRegistryrastrea todos los procesos hook activos,terminateAlllos mata de forma unificada al cancelar la tarea (HookProcessRegistry:17). - Lanzador multiplataforma:
getHookLaunchConfigdistingue 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,
streamCallbackañ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
Hooks interface:102— nombres de los 9 hooks + tipo de datos de entrada de cada uno.HookRunner:165— runner abstracto, su métodorunes stateless y reutilizable.completeParams:198— añade clineVersion / hookName / timestamp / workspaceRoots / userId a la entrada del hook.ConcreteHookRunner exec:289— método central que arranca un HookProcess y ejecuta el script.new HookProcess:324— crea el subproceso hook, enlaza abort signal y streamCallback.honor JSON regardless of exit code:454— el JSON válido tiene prioridad sobre el exitCode.executeHook:58— entrada, maneja errores, estado de UI y semántica de cancel de forma unificada.reorderHookAndToolMessages:109— en PreToolUse coloca la UI del hook por encima de la UI de la herramienta.cancel handling:166— al recibircancel: truemarca el estado del hook como cancelled y devuelve.HookProcess:89— hereda de EventEmitter, lanza el subproceso y emite por líneas.HookProcess.run:123— hace spawn del subproceso y enlaza stdin/stdout/stderr/exit.timeout:187— SIGTERM al expirar, el mensaje de error incluye la ruta del script.HookProcessRegistry:17— registro estático, terminate por lotes al cancelar la tarea.PreToolUseHookCancellationError:5— clase de excepción específica para que el catch de arriba distinga cancelación de hook de un fallo normal.emitNotificationHook:50— wrapper fire-and-forget del hook Notification.PreToolUse caller:73— llamada al hook antes de ejecutar la herramienta; cancel lanza excepción y cancelTask.PostToolUse caller:471— llamada al hook tras ejecutar la herramienta; cancel solo interrumpe lo siguiente.
Flujo de datos
Tomando PreToolUse como ejemplo, esta es la cadena «interrumpible» más completa. ToolExecutor, antes de ejecutar la herramienta, invoca 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")
}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:
// 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
cancelTaskpara detener todo el agent loop (cancelTask on cancel:100). - El cancel de PostToolUse no aborta la tarea:
runPostToolUseHookal 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:
HookProcessescucha 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.scriptPathy los milisegundos de timeout, para que el usuario ubique qué hook se colgó (timeout error:192). - Salida limitada a 1 MB:
MAX_HOOK_OUTPUT_SIZEcorta salidas infinitas; al superarlo emite un aviso de truncamiento (output truncation:311). - Truncado de contextModification:
MAX_CONTEXT_MODIFICATION_SIZEevita que un hook inyecte contexto desproporcionado; al superar el límite se trunca y avisa (context truncation:363). - PreToolUse se salta attempt_completion:
attempt_completiones 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.parsefalla,parseJsonOutputempareja 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.