Hooks: the user-hook execution chain
Responsibilities
Cline's hooks system lets users plug their own shell scripts into a handful of lifecycle points of the main agent loop (before submitting a prompt, before a tool runs, after a tool runs, task start/end, notification dispatched, before compaction). Scripts receive structured JSON input via stdin and can return JSON via stdout to steer Cline's behavior — most importantly, cancel: true blocks the imminent action, and contextModification appends extra context to the conversation. The whole mechanism has three layers: HookExecutor is the entry point that dispatches (executeHook:58), HookFactory + HookRunner handle script discovery and execution (HookRunner exec:289), and HookProcess manages the underlying subprocess (HookProcess:89).
It spans the entire Task lifecycle. PreToolUse / PostToolUse are called by ToolExecutor before and after tool execution (PreToolUse executeHook:73); UserPromptSubmit / TaskStart / TaskResume / TaskCancel are called by the main Task at the corresponding lifecycle points (UserPromptSubmit:1214); Notification is dispatched centrally by the NotificationHook module (emitNotificationHook:50). The "blockable" semantics of a hook only apply at cancellable points; fire-and-forget hooks like Notification ignore a returned cancel.
Design motivation
- Nine standard hooks:
PreToolUse/PostToolUse/UserPromptSubmit/TaskStart/TaskResume/TaskCancel/TaskComplete/Notification/PreCompact(Hooks interface:102), covering every key lifecycle point. - JSON in / JSON out: the stdin a hook script receives is JSON with metadata such as
clineVersion/hookName/timestamp/workspaceRoots/userId(completeParams:198), and the output is JSON too. - Three output fields:
cancelblocks an action,contextModificationappends context, anderrorMessageattaches an error explanation (HookExecutionResult:33). - A cancellable flag: the caller decides whether a hook can cancel an action;
NotificationpassesisCancellable: false(not cancellable:61), while PreToolUse passes true. - Process isolation + registry:
HookProcessRegistrytracks every active hook process, andterminateAllkills them all when a task is cancelled (HookProcessRegistry:17). - Cross-platform launcher:
getHookLaunchConfigdistinguishes Windows PowerShell from Unix shebang (getHookLaunchConfig:44), so.sh/.ps1/.jsscripts all run. - Stdout streamed back to the UI: HookProcess emits stdout/stderr line by line, and
streamCallbackprefixes each line with[source stream path]before echoing it to the UI (streamCallback:124), so even long-running hooks show progress. - JSON extraction fallback: when a hook script interleaves debug logs before or after the JSON, the parser scans backward matching brackets to find the last complete JSON object (
JSON extraction:379).
Key files
Hooks interface:102— the nine hook names plus each one's input data type.HookRunner:165— the abstract runner; itsrunmethod is stateless and reusable.completeParams:198— augments hook input with clineVersion / hookName / timestamp / workspaceRoots / userId.ConcreteHookRunner exec:289— the core method that actually starts a HookProcess and runs the script.new HookProcess:324— creates a hook subprocess and binds the abort signal and streamCallback.honor JSON regardless of exit code:454— valid JSON takes priority over exitCode.executeHook:58— entry point; unifies error handling, UI state, and cancel semantics.reorderHookAndToolMessages:109— for PreToolUse, orders the hook UI above the tool UI.cancel handling:166— oncancel: true, marks the hook state as cancelled and returns.HookProcess:89— an EventEmitter that spawns a subprocess and emits line by line.HookProcess.run:123— actually spawns the subprocess and wires up stdin/stdout/stderr/exit.timeout:187— SIGTERM on timeout; the error message includes the script path.HookProcessRegistry:17— a static registry that batch-terminates on task cancel.PreToolUseHookCancellationError:5— a dedicated exception class so upper layers can tell hook-driven cancellations apart from ordinary failures.emitNotificationHook:50— fire-and-forget wrapper for the Notification hook.PreToolUse caller:73— the pre-tool hook invocation; on cancel it throws and calls cancelTask.PostToolUse caller:471— the post-tool hook invocation; on cancel it just aborts what follows.
Data flow
Taking PreToolUse as the example, since it's the most complete "blockable" chain. Before actually running a tool, ToolExecutor calls 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")
}This sits near PreToolUse caller:73. Inside executeHook it first checks hooksEnabled (early return:72); if disabled it returns immediately. Then HookFactory.hasHook checks whether a script is configured for this hook name (hasHook:80); if not, it also returns. When configured, it creates a HookProcess and spawns the subprocess:
// 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()This sits near spawn child:176. After the subprocess exits, HookRunner.exec feeds stdout to parseJsonOutput (parseJsonOutput:349); if there's valid JSON it follows the JSON (even when exitCode is non-zero, JSON wins) (JSON priority:454); otherwise it falls back to exitCode. cancel: true propagates back up through HookExecutor.executeHook, and ToolHookUtils throws PreToolUseHookCancellationError and calls cancelTask.
Boundaries and failure modes
- PreToolUse cancel aborts the whole task: it doesn't just skip this tool — it calls
cancelTaskand stops the entire agent loop (cancelTask on cancel:100). - PostToolUse cancel does not abort the task:
runPostToolUseHookon cancel only says an error and returns true; the tool already ran and can't be rolled back (post cancel soft:494). - Notification hook ignores cancel and contextModification: it's a one-way notification, so output fields are explicitly ignored (
ignore unsupported output:70). - Abort signal rejects immediately:
HookProcesslistens to abortSignal; on abort it kills the subprocess and rejects without waiting forclose(abortHandler:139). - Timeout error includes the script path: the timeout error message carries
this.scriptPathand the timeout in milliseconds, so users can pinpoint which hook hung (timeout error:192). - 1 MB output cap:
MAX_HOOK_OUTPUT_SIZEcuts off unbounded output; beyond it, a truncation notice is emitted (output truncation:311). - contextModification truncation:
MAX_CONTEXT_MODIFICATION_SIZEprevents hooks from injecting oversized context; beyond it the content is truncated with a notice (context truncation:363). - PreToolUse skips attempt_completion:
attempt_completionis the finishing tool and isn't subject to PreToolUse blocking (skip attempt_completion:30). - NoOp hook returns proto defaults: a hook without a configured script returns
cancel: false / contextModification: "" / errorMessage: ""; the executor treats this as a normal no-op (NoOp runner:238). - JSON extraction scans right-to-left: when a hook script's leading debug logs make the first
JSON.parsefail,parseJsonOutputwalks brackets to find the last complete object (brace scan:385); this fallback is essential.
Summary
The hooks system is the extension slot Cline leaves for users to plug scripts into key points of the agent loop. To see the moments at which the main Task fires hooks, read agent-loop/task-class; to see how the tool executor threads PreToolUse / PostToolUse together, read cap-tools; to see how StateManager supplies metadata like workspaceRoots to hooks, read state-manager.
See official docs: Cline docs · README