Hooks : chaîne d'exécution des crochets utilisateur
Responsabilités
Le système de hooks (crochets) de Cline permet à l'utilisateur d'insérer ses propres scripts shell à plusieurs points de vie du boucle principale de l'agent (avant la soumission du prompt, avant/après usage d'un outil, au début/fin d'une tâche, à l'envoi d'une notification, avant compression). Le script reçoit un JSON structuré sur stdin, et peut renvoyer un JSON via stdout pour piloter le comportement de Cline — notamment cancel: true pour bloquer une action imminente, et contextModification pour injecter du contexte additionnel dans la conversation. Le tout repose sur trois couches : HookExecutor sert d'entrée et de dispatch (executeHook:58), HookFactory + HookRunner gèrent la découverte et l'exécution du script concret (HookRunner exec:289), et HookProcess pilote le sous-processus sous-jacent (HookProcess:89).
Le système traverse tout le cycle de vie de Task. PreToolUse / PostToolUse sont appelés par ToolExecutor avant/après exécution d'un outil (PreToolUse executeHook:73). UserPromptSubmit / TaskStart / TaskResume / TaskCancel sont appelés par Task aux points de vie correspondants (UserPromptSubmit:1214). Notification est émis uniformément par le module NotificationHook (emitNotificationHook:50). La sémantique « bloquable » d'un hook ne s'applique qu'aux points annulables : un hook fire-and-forget comme Notification voit son cancel ignoré même s'il le renvoie.
Motivation de conception
- 9 hooks standards :
PreToolUse/PostToolUse/UserPromptSubmit/TaskStart/TaskResume/TaskCancel/TaskComplete/Notification/PreCompact(Hooks interface:102), couvrent tous les points de vie critiques. - JSON in / JSON out : le stdin reçu par le script hook est un JSON portant
clineVersion/hookName/timestamp/workspaceRoots/userIdet autres métadonnées (completeParams:198), la sortie est également du JSON. - Trois champs de retour :
cancelbloque l'action,contextModificationajoute du contexte,errorMessageajoute un message d'erreur (HookExecutionResult:33). - Flag cancellable : l'appelant décide si ce hook peut annuler l'action.
NotificationpasseisCancellable: false(not cancellable:61), PreToolUse passe true. - Isolation des processus + registry :
HookProcessRegistrysuit tous les processus hook actifs,terminateAllles tue uniformément à l'annulation d'une tâche (HookProcessRegistry:17). - Lanceur multi-plateforme :
getHookLaunchConfigdistingue Windows PowerShell et Unix shebang (getHookLaunchConfig:44), de sorte que.sh/.ps1/.jsfonctionnent tous. - Écho streaming du stdout : HookProcess émet stdout/stderr ligne par ligne, et
streamCallbackajoute un préfixe[source stream path]avant de les renvoyer à l'UI (streamCallback:124), pour qu'un hook long puisse montrer sa progression. - Repli sur l'extraction JSON : quand le script hook imprime du debug avant/après le JSON, on scanne à partir de la fin en appariant les accolades pour trouver le dernier objet JSON complet (
JSON extraction:379).
Fichiers clés
Hooks interface:102— 9 noms de hooks + le type de données d'entrée de chacun.HookRunner:165— runner abstrait, la méthoderunest stateless et réutilisable.completeParams:198— complète l'entrée du hook avec clineVersion / hookName / timestamp / workspaceRoots / userId.ConcreteHookRunner exec:289— la méthode centrale qui lance HookProcess pour exécuter le script.new HookProcess:324— crée le sous-processus hook, branche abort signal et streamCallback.honor JSON regardless of exit code:454— un JSON valide prime sur exitCode.executeHook:58— entrée, gère uniformément erreur / état UI / sémantique cancel.reorderHookAndToolMessages:109— pour PreToolUse, place l'UI du hook au-dessus de l'UI de l'outil.cancel handling:166— à la réception decancel: true, marque l'état du hook cancelled et retourne.HookProcess:89— hérite d'EventEmitter, exécute le sous-processus et émet ligne par ligne.HookProcess.run:123— spawn réellement le sous-processus, branche stdin/stdout/stderr/exit.timeout:187— SIGTERM à l'expiration du délai, le message d'erreur contient le chemin du script.HookProcessRegistry:17— registry statique, terminate batch à l'annulation d'une tâche.PreToolUseHookCancellationError:5— classe d'exception dédiée, pour que le catch en amont puisse distinguer l'annulation par hook d'un échec ordinaire.emitNotificationHook:50— wrapping fire-and-forget du hook Notification.PreToolUse caller:73— appel du hook avant exécution de l'outil, cancel lève une exception et cancelTask.PostToolUse caller:471— appel du hook après exécution de l'outil, cancel interrompt seulement la suite.
Flux de données
Prenons PreToolUse comme exemple, c'est la chaîne « bloquable » la plus complète. ToolExecutor appelle d'abord ToolHookUtils.executePreToolUseHook avant d'exécuter réellement l'outil :
// 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")
}Ce bloc se trouve près de PreToolUse caller:73. Une fois entré dans executeHook, on regarde d'abord hooksEnabled (early return:72) : si désactivé, on retourne directement. Puis HookFactory.hasHook vérifie si un script est configuré pour ce nom de hook (hasHook:80) ; si non, retour direct également. Si oui, on crée HookProcess et on spawn le sous-processus :
// 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()Ce bloc se trouve près de spawn child:176. Une fois le sous-processus terminé, HookRunner.exec passe le stdout à parseJsonOutput (parseJsonOutput:349). S'il y a un JSON valide, on suit le JSON (même si exitCode n'est pas 0, le JSON prime — JSON priority:454) ; sinon, on se rabat sur exitCode. cancel: true remonte jusqu'à HookExecutor.executeHook, et ToolHookUtils à la réception lève PreToolUseHookCancellationError et appelle cancelTask.
Limites et échecs
- PreToolUse cancel aborte toute la tâche : pas seulement ignorer l'outil, mais appeler
cancelTaskpour stopper toute la boucle d'agent (cancelTask on cancel:100). - PostToolUse cancel sans abort de tâche :
runPostToolUseHookà la réception d'un cancel se contente de say error et renvoie true. L'outil a déjà été exécuté et n'est pas annulable (post cancel soft:494). - Hook Notification ignore cancel et contextModification : ce hook est une notification unidirectionnelle, les champs de sortie sont explicitement ignorés (
ignore unsupported output:70). - abort signal reject immédiat :
HookProcessécoute abortSignal. Dès l'abort, il tue le sous-processus et reject, sans attendre close (abortHandler:139). - Timeout signalé par chemin de script : le message d'erreur de timeout contient
this.scriptPathet la durée en ms, pour aider à localiser quel hook est resté bloqué (timeout error:192). - Limite de sortie 1 Mo :
MAX_HOOK_OUTPUT_SIZEintercepte les sorties infinies. Au-delà, on émet une notification de troncation (output truncation:311). - Troncation de contextModification :
MAX_CONTEXT_MODIFICATION_SIZEempêche un hook d'injecter un contexte démesuré, au-delà on tronque avec notification (context truncation:363). - PreToolUse saute attempt_completion :
attempt_completionest l'outil de fin, il ne passe pas par le blocage PreToolUse (skip attempt_completion:30). - Hook NoOp renvoie les defaults proto : un hook sans script trouvé renvoie
cancel: false / contextModification: "" / errorMessage: "". L'executor le considère comme un no-op normal (NoOp runner:238). - Extraction JSON scannée de droite à gauche : quand le script hook imprime du debug devant le JSON et que le premier
JSON.parseéchoue,parseJsonOutputappareille les accolades pour trouver le dernier objet complet (brace scan:385). Ce repli est crucial.
Résumé
Le système de hooks est l'extension que Cline laisse à l'utilisateur pour « insérer un script aux points clés de la boucle d'agent ». Pour voir à quels moments Task déclenche un hook, lire agent-loop/task-class ; pour voir comment l'exécuteur d'outils enchaîne PreToolUse / PostToolUse, lire cap-tools ; pour voir comment StateManager fournit à un hook les métadonnées comme workspaceRoots, lire state-manager.
Voir la documentation officielle : documentation Cline · README.