Hooks: Ausführungskette für Benutzer-Hooks
Verantwortung
Das Hooks-System von Cline erlaubt es Benutzern, an mehreren Lebenspunkten des Haupt-Agent-Loops (vor dem Absenden eines Prompts, vor und nach der Werkzeugnutzung, bei Task-Start/-Ende, beim Versand von Benachrichtigungen, vor der Komprimierung) eigene Shell-Skripte einzuhängen. Skripte lesen strukturiertes JSON über stdin, können über stdout JSON zurückgeben und damit das Verhalten von Cline steuern – am wichtigsten sind cancel: true (blockiert eine bevorstehende Aktion) und contextModification (hängt zusätzlichen Kontext an die Konversation an). Der Mechanismus hat drei Schichten: HookExecutor als Einstieg und Verteiler (executeHook:58), HookFactory + HookRunner für Entdeckung und Ausführung konkreter Skripte (HookRunner exec:289), und HookProcess verwaltet den untergeordneten Subprozess (HookProcess:89).
Es erstreckt sich über den gesamten Task-Lebenszyklus. PreToolUse / PostToolUse werden vom ToolExecutor vor beziehungsweise nach der Werkzeugausführung aufgerufen (PreToolUse executeHook:73); UserPromptSubmit / TaskStart / TaskResume / TaskCancel durch die Haupt-Task an den entsprechenden Lebenspunkten (UserPromptSubmit:1214); Notification wird einheitlich durch das NotificationHook-Modul versandt (emitNotificationHook:50). Die «abbrechbare» Semantik eines Hooks gilt nur an abbrechbaren Punkten; ein Fire-and-Forget-Hook wie Notification ignoriert ein zurückgegebenes cancel.
Entwurfsmotivation
- 9 Standard-Hooks:
PreToolUse/PostToolUse/UserPromptSubmit/TaskStart/TaskResume/TaskCancel/TaskComplete/Notification/PreCompact(Hooks interface:102)`, decken alle entscheidenden Lebenspunkte ab. - JSON rein / JSON raus: Der stdin eines Hook-Skripts ist ein JSON mit Metadaten wie
clineVersion/hookName/timestamp/workspaceRoots/userId(completeParams:198); die Ausgabe ist ebenfalls JSON. - Drei Rückgabefelder:
cancelblockiert eine Aktion,contextModificationhängt Kontext an,errorMessageliefert eine Fehlerbeschreibung (HookExecutionResult:33). - cancellable-Flag: Der Aufrufer entscheidet, ob dieser Hook eine Aktion abbrechen darf;
Notificationwird mitisCancellable: falseaufgerufen (not cancellable:61)`, PreToolUse mit true. - Prozess-Isolation + Registry:
HookProcessRegistryverfolgt alle aktiven Hook-Prozesse;terminateAllkillt sie beim Task-Abbruch einheitlich (HookProcessRegistry:17). - Plattformübergreifender Launcher:
getHookLaunchConfigunterscheidet zwischen Windows PowerShell und Unix-Shebang (getHookLaunchConfig:44), sodass Benutzer.sh/.ps1/.jsausführen können. - stdout wird zeilenweise zurückgespiegelt: HookProcess emittiert stdout/stderr zeilenweise;
streamCallbackversieht sie mit dem Präfix[source stream path]und spiegelt sie ins UI (streamCallback:124)`, sodass auch lange Hooks Fortschritt zeigen. - JSON-Extraktion mit Fallback: Wenn ein Hook-Skript vor und nach dem JSON Debug-Logs ausgibt, wird von hinten nach vorn nach Klammersetzung gesucht, um das letzte vollständige JSON-Objekt zu finden (
JSON extraction:379).
Schlüsseldateien
Hooks interface:102— Die 9 Hook-Namen plus die jeweiligen Eingabedatentypen.HookRunner:165— Abstrakter Runner; dierun-Methode ist zustandlos und wiederverwendbar.completeParams:198— Ergänzt clineVersion / hookName / timestamp / workspaceRoots / userId in der Hook-Eingabe.ConcreteHookRunner exec:289— Die Kernmethode, die tatsächlich einen HookProcess startet und das Skript ausführt.new HookProcess:324— Erzeugt den Hook-Subprozess, bindet Abort-Signal und streamCallback.honor JSON regardless of exit code:454— Gültiges JSON hat Vorrang vor exitCode.executeHook:58— Einstieg; vereinheitlicht Fehlerbehandlung, UI-Zustand und cancel-Semantik.reorderHookAndToolMessages:109— Ordnet bei PreToolUse die Hook-UI über die Werkzeug-UI.cancel handling:166— Setzt beicancel: trueden Hook-Status auf cancelled und kehrt zurück.HookProcess:89— Erweitert EventEmitter, führt den Subprozess aus und emittiert zeilenweise.HookProcess.run:123— Spawn-Logik für den Subprozess; bindet stdin/stdout/stderr/exit.timeout:187— SIGTERM bei Timeout; die Fehlermeldung trägt den Skriptpfad.HookProcessRegistry:17— Statische Registry; beim Task-Abbruch werden alle Prozesse gemeinsam terminiert.PreToolUseHookCancellationError:5— Spezielle Exception-Klasse, damit obere Catch-Blöcke zwischen Hook-Abbruch und gewöhnlichem Fehlschlag unterscheiden können.emitNotificationHook:50— Fire-and-Forget-Verpackung für den Notification-Hook.PreToolUse caller:73— Hook-Aufruf vor der Werkzeugausführung; bei cancel wird eine Exception geworfen und cancelTask aufgerufen.PostToolUse caller:471— Hook-Aufruf nach der Werkzeugausführung; cancel bricht nur die Fortsetzung ab.
Datenfluss
Am Beispiel von PreToolUse, der vollständigsten «abbrechbaren» Kette. Der ToolExecutor ruft vor der eigentlichen Werkzeugausführung ToolHookUtils.executePreToolUseHook auf:
// 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")
}Diese Stelle liegt in der Nähe von PreToolUse caller:73. executeHook prüft zuerst hooksEnabled (early return:72); ist es deaktiviert, kehrt es sofort zurück. Danach prüft HookFactory.hasHook, ob für diesen Hook-Namen ein Skript konfiguriert ist (hasHook:80); ist nichts konfiguriert, wird ebenfalls direkt zurückgekehrt. Ist ein Skript vorhanden, wird ein HookProcess` erzeugt und der Subprozess gespawnt:
// 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()Diese Stelle liegt in der Nähe von spawn child:176. Nachdem der Subprozess beendet ist, nimmt HookRunner.exec den stdout und gibt ihn an parseJsonOutput (parseJsonOutput:349); ist gültiges JSON vorhanden, wird dieses verwendet (selbst wenn exitCode nicht 0 ist) (JSON priority:454), ansonsten entscheidet der exitCode. cancel: truewandert durchHookExecutor.executeHookzurück;ToolHookUtilswirft daraufhin einePreToolUseHookCancellationErrorund ruftcancelTask` auf.
Grenzen und Fehler
- PreToolUse-cancel bricht die gesamte Task ab: Es wird nicht nur das Werkzeug übersprungen, sondern
cancelTaskaufgerufen und damit der ganze Agent-Loop gestoppt (cancelTask on cancel:100). - PostToolUse-cancel bricht die Task nicht ab:
runPostToolUseHookgibt bei cancel lediglich eine Fehlermeldung aus und kehrt zurück; das Werkzeug ist bereits ausgeführt und kann nicht zurückgerollt werden (post cancel soft:494). - Notification-Hook ignoriert cancel und contextModification: Dieser Hook ist eineEin-Weg-Benachrichtigung; seine Ausgabefelder werden explizit ignoriert (
ignore unsupported output:70). - abort-Signal lehnt sofort ab:
HookProcessüberwacht das abortSignal; bei Abort wird der Subprozess sofort gekillt und ein Reject erzeugt, ohne auf close zu warten (abortHandler:139). - Timeout meldet den Skriptpfad: Die Timeout-Fehlermeldung enthält
this.scriptPathund die Timeout-Millisekunden, damit der Benutzer erkennen kann, welcher Hook hängen bleibt (timeout error:192). - Ausgabe bis 1 MB begrenzt:
MAX_HOOK_OUTPUT_SIZEwehrt endlose Ausgaben ab; bei Überschreitung wird eine truncation-Meldung emittiert (output truncation:311). - contextModification wird abgeschnitten:
MAX_CONTEXT_MODIFICATION_SIZEverhindert, dass ein Hook massiv viel Kontext injiziert; bei Überschreitung wird abgeschnitten und ein Hinweis angefügt (context truncation:363). - PreToolUse überspringt attempt_completion:
attempt_completionist das abschliessende Werkzeug und durchläuft kein PreToolUse-Abbrechen (skip attempt_completion:30). - NoOp-Hook gibt Proto-Defaults zurück: Ein Hook ohne gefundenes Skript liefert
cancel: false / contextModification: "" / errorMessage: ""; der Executor betrachtet das als normales No-op (NoOp runner:238). - JSON-Extraktion von rechts nach links: Wenn Debug-Logs am Anfang des Hook-Skripts das erste
JSON.parsescheitern lassen, suchtparseJsonOutputmit Klammersetzung nach dem letzten vollständigen Objekt (brace scan:385)`; dieser Fallback ist entscheidend.
Zusammenfassung
Das Hooks-System ist die Erweiterungsstelle, die Cline Benutzern lässt, um an entscheidenden Punkten des Agent-Loops Skripte einzuhängen. Wer sehen will, an welchen Momenten die Haupt-Task Hooks triggert, kann agent-loop/task-class lesen; wie der Werkzeugexecutor PreToolUse / PostToolUse verknüpft, steht in cap-tools; wie der StateManager dem Hook Metadaten wie workspaceRoots liefert, steht in state-manager.
Siehe offizielle Dokumentation: Cline-Dokumentation · README