Skip to content

Hooks: Ausführungskette für Benutzer-Hooks

源码版本v4.0.10

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: cancel blockiert eine Aktion, contextModification hängt Kontext an, errorMessage liefert eine Fehlerbeschreibung (HookExecutionResult:33).
  • cancellable-Flag: Der Aufrufer entscheidet, ob dieser Hook eine Aktion abbrechen darf; Notification wird mit isCancellable: false aufgerufen (not cancellable:61)`, PreToolUse mit true.
  • Prozess-Isolation + Registry: HookProcessRegistry verfolgt alle aktiven Hook-Prozesse; terminateAll killt sie beim Task-Abbruch einheitlich (HookProcessRegistry:17).
  • Plattformübergreifender Launcher: getHookLaunchConfig unterscheidet zwischen Windows PowerShell und Unix-Shebang (getHookLaunchConfig:44), sodass Benutzer .sh / .ps1 / .js ausführen können.
  • stdout wird zeilenweise zurückgespiegelt: HookProcess emittiert stdout/stderr zeilenweise; streamCallback versieht 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

Datenfluss

Am Beispiel von PreToolUse, der vollständigsten «abbrechbaren» Kette. Der ToolExecutor ruft vor der eigentlichen Werkzeugausführung ToolHookUtils.executePreToolUseHook auf:

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

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:

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

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 cancelTask aufgerufen und damit der ganze Agent-Loop gestoppt (cancelTask on cancel:100).
  • PostToolUse-cancel bricht die Task nicht ab: runPostToolUseHook gibt 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.scriptPath und die Timeout-Millisekunden, damit der Benutzer erkennen kann, welcher Hook hängen bleibt (timeout error:192).
  • Ausgabe bis 1 MB begrenzt: MAX_HOOK_OUTPUT_SIZE wehrt endlose Ausgaben ab; bei Überschreitung wird eine truncation-Meldung emittiert (output truncation:311).
  • contextModification wird abgeschnitten: MAX_CONTEXT_MODIFICATION_SIZE verhindert, dass ein Hook massiv viel Kontext injiziert; bei Überschreitung wird abgeschnitten und ein Hinweis angefügt (context truncation:363).
  • PreToolUse überspringt attempt_completion: attempt_completion ist 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.parse scheitern lassen, sucht parseJsonOutput mit 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