- Accueil
- Documentation
- Configuration
- Hooks
Hooks
Ce document décrit le code actuel du sous-système de hooks dans src/extensibility/hooks/*.
État actuel dans le runtime
Section intitulée « État actuel dans le runtime »Le paquet de hooks (src/extensibility/hooks/) est toujours exporté et utilisable comme surface d’API, mais le runtime CLI par défaut initialise désormais le chemin du runner d’extension. Dans le flux de démarrage actuel :
--hookest traité comme un alias de--extension(les chemins CLI sont fusionnés dansadditionalExtensionPaths)- les outils sont encapsulés par
ExtensionToolWrapper, et non parHookToolWrapper - les transformations de contexte et les émissions de cycle de vie passent par
ExtensionRunner
Ce fichier documente donc l’implémentation du sous-système de hooks lui-même (types/loader/runner/wrapper), y compris le comportement et les contraintes hérités.
Fichiers clés
Section intitulée « Fichiers clés »src/extensibility/hooks/types.ts— contexte de hook, types d’événements et contrats de résultatsrc/extensibility/hooks/loader.ts— chargement de module et pont de découverte de hookssrc/extensibility/hooks/runner.ts— distribution d’événements, recherche de commandes et signalisation d’erreurssrc/extensibility/hooks/tool-wrapper.ts— wrapper d’interception d’outils pré/postsrc/extensibility/hooks/index.ts— exports/réexports
Ce qu’est un module hook
Section intitulée « Ce qu’est un module hook »Un module hook doit exporter par défaut une factory :
import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function hook(pi: HookAPI): void { pi.on("tool_call", async (event, ctx) => { if (event.toolName === "bash" && String(event.input.command ?? "").includes("rm -rf")) { return { block: true, reason: "blocked by policy" }; } });}La factory peut :
- enregistrer des gestionnaires d’événements avec
pi.on(...) - envoyer des messages personnalisés persistants avec
pi.sendMessage(...) - conserver un état non-LLM avec
pi.appendEntry(...) - enregistrer des commandes slash via
pi.registerCommand(...) - enregistrer des rendus de messages personnalisés via
pi.registerMessageRenderer(...) - exécuter des commandes shell via
pi.exec(...)
Découverte et chargement
Section intitulée « Découverte et chargement »discoverAndLoadHooks(configuredPaths, cwd) effectue les opérations suivantes :
- Charger les hooks découverts depuis le registre de capacités (
loadCapability("hooks")) - Ajouter les chemins configurés explicitement (dédupliqués par chemin absolu)
- Appeler
loadHooks(allPaths, cwd)
loadHooks importe ensuite chaque chemin et attend une fonction default.
Résolution de chemin
Section intitulée « Résolution de chemin »loader.ts résout les chemins de hooks comme suit :
- chemin absolu : utilisé tel quel
- chemin
~: développé - chemin relatif : résolu par rapport à
cwd
Incompatibilité héritée importante
Section intitulée « Incompatibilité héritée importante »Les fournisseurs de découverte pour hookCapability modélisent encore des fichiers de hooks shell de style pré/post (par exemple .claude/hooks/pre/*, .xcsh/.../hooks/pre/*).
Le chargeur de hooks ici utilise l’import de module dynamique et nécessite une factory de hook JS/TS par défaut. Si un chemin de hook découvert n’est pas importable en tant que module, le chargement échoue et est signalé dans LoadHooksResult.errors.
Surfaces d’événements
Section intitulée « Surfaces d’événements »Les événements de hook sont fortement typés dans types.ts.
Événements de session
Section intitulée « Événements de session »session_startsession_before_switch→ peut retourner{ cancel?: boolean }session_switchsession_before_branch→ peut retourner{ cancel?: boolean; skipConversationRestore?: boolean }session_branchsession_before_compact→ peut retourner{ cancel?: boolean; compaction?: CompactionResult }session.compacting→ peut retourner{ context?: string[]; prompt?: string; preserveData?: Record<string, unknown> }session_compactsession_before_tree→ peut retourner{ cancel?: boolean; summary?: { summary: string; details?: unknown } }session_treesession_shutdown
Événements agent/contexte
Section intitulée « Événements agent/contexte »context→ peut retourner{ messages?: Message[] }before_agent_start→ peut retourner{ message?: { customType; content; display; details } }agent_startagent_endturn_startturn_endauto_compaction_startauto_compaction_endauto_retry_startauto_retry_endttsr_triggeredtodo_reminder
Événements d’outils (modèle pré/post)
Section intitulée « Événements d’outils (modèle pré/post) »tool_call(pré-exécution) → peut retourner{ block?: boolean; reason?: string }tool_result(post-exécution) → peut retourner{ content?; details?; isError? }
Il s’agit du modèle d’interception pré/post au cœur du sous-système de hooks.
Flux d'interception des outils par les hooks
Gestionnaires tool_call │ ├─ any { block: true }? ── oui ──> throw (outil bloqué) │ └─ non │ ▼ exécution de l'outil sous-jacent │ ├─ succès ──> les gestionnaires tool_result peuvent remplacer { content, details } │ └─ erreur ──> émettre tool_result(isError=true) puis relancer l'erreur d'origineModèle d’exécution et sémantique de mutation
Section intitulée « Modèle d’exécution et sémantique de mutation »1) Pré-exécution : tool_call
Section intitulée « 1) Pré-exécution : tool_call »HookToolWrapper.execute() émet tool_call avant l’exécution de l’outil.
- si un gestionnaire retourne
{ block: true }, l’exécution s’arrête - si un gestionnaire lève une exception, le wrapper échoue de façon sécurisée et bloque l’exécution
- le
reasonretourné devient le texte de l’erreur levée
2) Exécution de l’outil
Section intitulée « 2) Exécution de l’outil »L’outil sous-jacent s’exécute normalement s’il n’est pas bloqué.
3) Post-exécution : tool_result
Section intitulée « 3) Post-exécution : tool_result »Après un succès, le wrapper émet tool_result avec :
toolName,toolCallId,inputcontentdetailsisError: false
Si un gestionnaire retourne des substitutions :
contentpeut remplacer le contenu du résultatdetailspeut remplacer les détails du résultat
En cas d’échec de l’outil, le wrapper émet tool_result avec isError: true et le texte d’erreur en contenu, puis relance l’erreur d’origine.
Ce que les hooks peuvent muter
Section intitulée « Ce que les hooks peuvent muter »- le contexte LLM pour un seul appel via
context(chaîne de remplacement demessages) - le contenu/détails de sortie d’outil lors d’appels d’outils réussis (chemin
tool_result) - le message injecté avant l’agent via
before_agent_start - le comportement d’annulation/compaction personnalisée/arbre via
session_before_*etsession.compacting
Ce que les hooks ne peuvent pas muter dans cette implémentation
Section intitulée « Ce que les hooks ne peuvent pas muter dans cette implémentation »- les paramètres d’entrée bruts de l’outil en place (uniquement bloquer/autoriser sur
tool_call) - la continuation de l’exécution après des erreurs d’outil levées (le chemin d’erreur relance)
- le statut final de succès/erreur dans le comportement du wrapper (le
isErrorretourné est typé mais non appliqué parHookToolWrapper)
Ordre et comportement en cas de conflit
Section intitulée « Ordre et comportement en cas de conflit »Ordre au niveau de la découverte
Section intitulée « Ordre au niveau de la découverte »Les fournisseurs de capacités sont triés par priorité (les plus élevées en premier). La déduplication se fait par clé de capacité, le premier l’emporte.
Pour hooks, la clé de capacité est ${type}:${tool}:${name}. Les doublons masqués provenant de fournisseurs de priorité inférieure sont marqués et exclus de la liste découverte effective.
Ordre de chargement
Section intitulée « Ordre de chargement »discoverAndLoadHooks construit une liste plate allPaths, dédupliquée par chemin absolu résolu, puis loadHooks itère dans cet ordre.
L’ordre des fichiers dans chaque répertoire découvert dépend de la sortie de readdir ; le chargeur de hooks n’effectue pas de tri supplémentaire.
Ordre des gestionnaires au runtime
Section intitulée « Ordre des gestionnaires au runtime »Dans HookRunner, l’ordre est déterministe par séquence d’enregistrement :
- ordre du tableau des hooks
- ordre d’enregistrement des gestionnaires par hook/événement
Comportement en cas de conflit par type d’événement :
tool_call: le dernier résultat retourné l’emporte sauf si un gestionnaire bloque ; le premier blocage court-circuitetool_result: la dernière substitution retournée l’emporte (pas de court-circuit)context: chaîné ; chaque gestionnaire reçoit la sortie de messages du gestionnaire précédentbefore_agent_start: le premier message retourné est conservé ; les messages ultérieurs sont ignoréssession_before_*: le dernier résultat retourné est suivi ;cancel: truecourt-circuite immédiatementsession.compacting: le dernier résultat retourné l’emporte
Conflits de commandes/rendus :
getCommand(name)retourne la première correspondance parmi les hooks (le premier chargé l’emporte)getMessageRenderer(customType)retourne la première correspondancegetRegisteredCommands()retourne toutes les commandes (sans déduplication)
Interactions UI (HookContext.ui)
Section intitulée « Interactions UI (HookContext.ui) »HookUIContext inclut :
select,confirm,input,editornotifysetStatuscustomsetEditorText,getEditorText- getter
theme
ctx.hasUI indique si une interface utilisateur interactive est disponible.
Lors d’une exécution sans UI, le comportement par défaut du contexte sans opération est :
select/input/editorretournentundefinedconfirmretournefalsenotify,setStatus,setEditorTextsont des no-opsgetEditorTextretourne""
Comportement de la ligne de statut
Section intitulée « Comportement de la ligne de statut »Le texte de statut de hook défini via ctx.ui.setStatus(key, text) est :
- stocké par clé
- trié par nom de clé
- assaini (
\r,\n,\t→ espaces ; espaces répétés réduits) - joint et tronqué en largeur pour l’affichage
Propagation des erreurs et repli
Section intitulée « Propagation des erreurs et repli »Au chargement
Section intitulée « Au chargement »- module invalide ou export par défaut manquant → capturé dans
LoadHooksResult.errors - le chargement continue pour les autres hooks
Au moment de l’événement
Section intitulée « Au moment de l’événement »HookRunner.emit(...) capture les erreurs de gestionnaire pour la plupart des événements et émet HookError aux écouteurs (hookPath, event, error), puis continue.
emitToolCall(...) est plus strict : les erreurs de gestionnaire n’y sont pas absorbées ; elles se propagent à l’appelant. Dans HookToolWrapper, cela bloque l’appel d’outil (sécurité par défaut).
Exemples d’API réalistes
Section intitulée « Exemples d’API réalistes »Bloquer les commandes bash dangereuses
Section intitulée « Bloquer les commandes bash dangereuses »import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.on("tool_call", async (event, ctx) => { if (event.toolName !== "bash") return; const cmd = String(event.input.command ?? ""); if (!cmd.includes("rm -rf")) return;
if (!ctx.hasUI) return { block: true, reason: "rm -rf blocked (no UI)" }; const ok = await ctx.ui.confirm("Dangerous command", `Allow: ${cmd}`); if (!ok) return { block: true, reason: "user denied command" }; });}Masquer la sortie d’outil en post-exécution
Section intitulée « Masquer la sortie d’outil en post-exécution »import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.on("tool_result", async event => { if (event.toolName !== "read" || event.isError) return;
const redacted = event.content.map(chunk => { if (chunk.type !== "text") return chunk; return { ...chunk, text: chunk.text.replaceAll(/API_KEY=\S+/g, "API_KEY=[REDACTED]") }; });
return { content: redacted }; });}Modifier le contexte du modèle par appel LLM
Section intitulée « Modifier le contexte du modèle par appel LLM »import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.on("context", async event => { const filtered = event.messages.filter(msg => !(msg.role === "custom" && msg.customType === "debug-only")); return { messages: filtered }; });}Enregistrer une commande slash avec des méthodes de contexte sécurisées pour les commandes
Section intitulée « Enregistrer une commande slash avec des méthodes de contexte sécurisées pour les commandes »import type { HookAPI } from "@f5-sales-demo/xcsh/hooks";
export default function (pi: HookAPI): void { pi.registerCommand("handoff", { description: "Create a new session with setup message", handler: async (_args, ctx) => { await ctx.waitForIdle(); await ctx.newSession({ parentSession: ctx.sessionManager.getSessionFile(), setup: async sm => { sm.appendMessage({ role: "user", content: [{ type: "text", text: "Continue from prior session summary." }], timestamp: Date.now(), }); }, }); }, });}Surface d’export
Section intitulée « Surface d’export »src/extensibility/hooks/index.ts exporte :
- les API de chargement (
discoverAndLoadHooks,loadHooks) - le runner et le wrapper (
HookRunner,HookToolWrapper) - tous les types de hooks
- réexport de
execCommand
Et la racine du paquet (src/index.ts) réexporte les types de hooks comme surface de compatibilité héritée.