Hooks
Voici le problème avec le fait de dire à Claude de faire quelque chose dans un fichier CLAUDE.md : c'est une demande, pas une garantie. Vous pouvez écrire « toujours formater après modification » et Claude écoutera généralement. Généralement. Mais lors d'une longue exécution que vous ne surveillez pas, « généralement » ne suffit pas. Un hook résout ce problème. Un hook est du code déterministe qui s'exécute à un point fixe de la boucle, ce qui lui permet de garantir un comportement au lieu d'espérer qu'il se produise. Cela transforme une règle de « Claude écoute généralement » en « Claude ne peut pas l'ignorer ».
C'est tout l'argument. Voyons maintenant comment cela fonctionne réellement.
Les événements de hook
Claude Code déclenche environ 30 événements de hook au cours d'une session. Vous n'avez pas besoin de tous les connaître. Il y en a une petite poignée que vous utiliserez encore et encore, et ils correspondent à des points de la boucle agentique où vous voudriez intervenir.
Voici comment ils s'intègrent dans la boucle. Une session démarre, des requêtes arrivent, des outils sont appelés, et le tour finit par se terminer. Chacun de ces moments dispose d'un hook sur lequel vous pouvez accrocher du code.
Ceux qu'il vaut la peine de connaître :
- PreToolUse se déclenche avant un appel d'outil. C'est votre primitive d'application des règles. C'est celle qui peut arrêter quelque chose avant que cela ne se produise.
- PostToolUse se déclenche après un appel d'outil réussi. C'est généralement là que se trouve le formatage automatique ou le lint automatique.
- Stop se déclenche lorsque Claude veut terminer son tour. Vous pouvez refuser et dire « non, vous n'avez pas encore terminé » si une condition n'est pas remplie. Il existe un équivalent, SubagentStop, pour lorsqu'un sous-agent termine.
- PreCompact et PostCompact se déclenchent avant et après la compaction.
- InstructionsLoaded se déclenche lorsqu'un fichier CLAUDE.md ou un fichier de règles se charge. Pratique pour vérifier ce qui a réellement été intégré au contexte.
- SessionStart se déclenche au démarrage et prépare l'environnement. Utilisez la source
startupsi vous ne le souhaitez que pour les démarrages à froid.
Un point qui déroute souvent : pour réinjecter du contexte après une compaction, n'utilisez pas PostCompact. Utilisez SessionStart avec le matcher compact. C'est celui-là qui fait réellement revenir sa sortie dans la conversation.
PreToolUse : renvoyer une décision au format JSON
C'est dans PreToolUse que réside la véritable puissance, car il peut bloquer un appel d'outil avant son exécution. Pour répondre à Claude, vous affichez du JSON et quittez avec le code zéro. Le champ clé est permissionDecision, et il prend l'une des trois valeurs suivantes :
allow— laisser passer l'appeldeny— arrêter l'appelask— renvoyer la décision à l'utilisateur
Il existe techniquement une quatrième valeur, defer, mais elle ne s'applique qu'aux exécutions non interactives -p, où un processus appelant met l'outil en pause et le reprend plus tard. Vous y aurez rarement recours.
Voici à quoi cela ressemble :
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "...",
"updatedInput": {
"command": "..."
}
}
}Remarquez updatedInput. Au lieu de bloquer un appel, vous pouvez le réécrire. C'est ainsi que vous pourriez masquer un secret dans une commande bash tout en la laissant s'exécuter. Un piège : updatedInput remplace l'objet d'entrée entier, vous devez donc renvoyer les champs que vous ne modifiez pas, sous peine de les perdre.
Codes de sortie, pour les hooks qui ne renvoient pas de JSON
Tous les hooks n'ont pas besoin de parler JSON. Pour les hooks plus simples, les codes de sortie font l'affaire. Trois chiffres comptent.
- 0 signifie succès. Si la sortie standard est du JSON, Claude l'analyse. Le texte brut est ignoré pour la plupart des événements, mais pour SessionStart, UserPromptSubmit et UserPromptExpansion, le texte brut est ajouté au contexte. C'est exactement ce qui permet à un hook de préservation d'état de fonctionner.
- 2 est une erreur bloquante. L'erreur standard est renvoyée à Claude sous forme de contexte. C'est le code de sortie bloquant presque partout.
- Tout autre code est non bloquant. L'erreur standard est consignée, et Claude poursuit.
Celui qui piège les gens, c'est le code de sortie 1. Il donne l'impression d'être une erreur, mais il ne bloque pas. Claude exécute quand même la commande. Donc si vous vouliez arrêter quelque chose, utilisez le code 2, pas le 1.
Encore quelques subtilités. Le code 2 peut même bloquer Stop, ce qui est la façon de dire à Claude qu'il n'a pas terminé. Mais PostToolUse se déclenche après que l'outil a déjà été exécuté, donc bloquer à ce moment-là est trop tard pour arrêter l'appel, même si cela peut encore renvoyer du texte à Claude. Et quelques événements ignorent complètement le blocage, comme Notification et SessionStart. SessionStart vous affiche votre erreur standard à vous seul, Notification l'ignore, et les deux poursuivent quoi qu'il arrive.
Un véritable garde-fou : masquer plutôt que bloquer
Rassemblons tout cela avec quelque chose de concret. Disons que vous voulez un garde-fou PreToolUse sur l'outil Bash. Le matcher choisit l'outil à surveiller, et une clause if facultative peut le restreindre à une commande spécifique.
La solution évidente consiste à renvoyer deny et à arrêter un appel dangereux. C'est bien. Mais la solution moins connue et plus intéressante consiste à renvoyer updatedInput pour réécrire l'appel. C'est ainsi que vous retirez un secret d'une commande tout en la laissant s'exécuter, au lieu de simplement la refuser.
Voici à quoi cela ressemble en pratique. On demande à Claude d'exécuter une commande qui contient un secret qui semble actif. Le hook l'intercepte, repère le motif sk_live_, et le remplace par un espace réservé avant même que la commande ne s'exécute.
La commande s'est tout de même exécutée. Le travail a tout de même été accompli. Mais le secret n'est jamais passé. C'est la différence entre bloquer et masquer, et c'est le genre de chose qu'un hook peut faire respecter à chaque fois.
Préserver l'état à travers une compaction
Encore un schéma qui vaut la peine d'être mis en place. Lorsque Claude compacte une longue conversation, il perd beaucoup de détails. Un hook SessionStart avec le matcher compact s'exécute juste après la compaction. Faites-lui afficher un court résumé des fichiers sur lesquels vous avez travaillé. Ce résumé revient dans le contexte, de sorte que Claude reprend là où il s'était arrêté au lieu de repartir de zéro.
Pour conclure
Les hooks transforment une règle que Claude suit généralement en une règle qu'il suit toujours. Allez au-delà du formatage automatique : protégez les outils avec PreToolUse, contrôlez le tour avec Stop, et préservez l'état à travers une compaction. La mise en place demande un peu d'effort au départ, mais elle est rentabilisée dès la première fois qu'elle détecte quelque chose lors d'une exécution que vous ne surveilliez même pas.