Claude Code Hooks : la CI pour ton agent

Personne n'écrit « please run the tests before you deploy » dans un README en espérant que la team s'y conforme. Tu le cables dans la CI, où ça tourne à chaque fois et qui se fiche de ce que chacun en pense.
Ton CLAUDE.md, c'est le README. Il est plein de règles que tu espères que l'agent suit, et la plupart du temps il le fait, jusqu'à ce qu'une longue session remplisse la context window et que la compaction droppe discrètement « always run prettier » pour faire de la place. Les hooks sont la CI. Ce sont des commandes que le harness exécute à des points fixes du lifecycle de Claude Code, à chaque fois, avec zéro intervention du modèle. C'est le principe auquel tu fais déjà confiance, pointé vers ton agent au lieu de tes teammates.
Ce qu'est vraiment un hook
Un hook fire à un point spécifique de la session : avant qu'un outil tourne, après qu'il a terminé, quand l'agent est sur le point de s'arrêter. Claude Code passe l'event à ton handler en JSON, le handler fait son travail, et un exit code ou une petite réponse JSON dit à Claude Code s'il doit continuer. Le modèle n'a pas son mot à dire.
Tu configures les hooks dans les settings, pas dans ton prompt. Mets-les dans .claude/settings.json pour les partager avec ta team, .claude/settings.local.json pour les personnels, ou ~/.claude/settings.json pour les appliquer partout. Voilà le plus petit qui soit utile, un formatter qui tourne après chaque edit :
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I{} npx prettier --write {}"
}
]
}
]
}
}Le matcher est un regex sur le nom de l'outil, donc Edit|Write fire sur les deux et rien d'autre. La commande lit l'event JSON depuis stdin, extrait le file path avec jq, et lance Prettier dessus. Pas de variable magique $CLAUDE_FILE_PATH à retenir. Les données arrivent sur stdin et tu prends ce dont tu as besoin.
La plupart des hooks sont des commandes shell comme celle-ci. Trois autres types de handlers prennent le relais quand tu dépasses le shell : prompt envoie une évaluation one-shot à un modèle rapide, agent spawn un subagent avec accès aux outils, et http poste l'event à un service. Commence avec command. Tape /hooks dans n'importe quelle session pour voir ce qui est actuellement câblé.
Les trois qui valent le coup d'être câblés
Après des mois d'itération, trois hooks gagnent leur place dans chaque projet sur lequel je bosse. Chacun couvre un moment où un agent dérive de façon fiable.
Format on edit. Celui du dessus. C'est le meilleur retour sur effort de tous les hooks que je fais tourner : cinq minutes de setup, et du code non formaté n'atteint plus jamais un diff. Il tourne pendant que l'agent travaille, donc l'agent voit des fichiers propres au fur et à mesure au lieu d'un formatter qui réécrit tout au moment du commit.
Block dangerous commands. Un hook PreToolUse sur Bash, pointé vers un script qui inspecte la commande avant qu'elle tourne :
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}Le matcher ne fait que restreindre à Bash. La vraie décision se fait dans le script, parce qu'un matcher ne peut pas voir les arguments de la commande :
#!/bin/bash
command=$(jq -r '.tool_input.command')
if echo "$command" | grep -qE 'rm -rf|git push --force|drop table'; then
echo "Blocked: matched a destructive pattern" >&2
exit 2
fi
exit 0Exit 0 laisse passer la commande. Exit 2 la bloque et renvoie ton message de stderr à Claude comme raison, pour qu'il comprenne ce qui s'est passé au lieu de se cogner à un mur. Si tu veux un contrôle plus fin, comme réécrire la commande ou demander confirmation au user, renvoie un petit objet JSON sur stdout au lieu de quitter avec 2. L'exit code, c'est le chemin simple, et c'est celui que j'attrape en premier.
Verify before stopping. Un hook Stop fire quand Claude décide qu'il a fini, ce qui est exactement le moment où un agent a le plus de chances de se tromper. Au lieu d'un script shell, pointe celui-ci vers un handler prompt et laisse un modèle rapide checker le travail :
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate whether Claude should stop. Context: $ARGUMENTS\n\nRespond {\"ok\": true} if the tests were run and every requested task is complete, or {\"ok\": false, \"reason\": \"...\"} to keep going.",
"timeout": 30
}
]
}
]
}
}$ARGUMENTS injecte les données de l'event, y compris le path vers le transcript, pour que le modèle puisse voir ce qui s'est réellement passé. S'il renvoie {"ok": false}, l'agent ne s'arrête pas. Il récupère la raison comme instruction suivante et continue. Plus de « j'ai fini les changes » posé sur une suite de tests qui n'a jamais tourné. Ce hook a changé ma façon de bosser plus que n'importe quelle ligne que j'ai jamais mise dans CLAUDE.md.
Ce qui appartient à un hook, et ce qui n'y appartient pas
Les hooks sont addictifs. Une fois qu'un marche, tu veux tout y mettre. La plupart de ton CLAUDE.md devrait rester exactement où il est.
La frontière, c'est une question de jugement. CLAUDE.md, c'est pour les règles que le modèle doit peser et appliquer en context : préférer la composition à l'héritage, utiliser les Server Components par défaut, aller chercher @jd/ui avant de crafter un composant à la main. Ça demande du raisonnement, et le raisonnement, c'est à ça que sert le modèle. Les hooks, c'est pour les règles sans aucun jugement : formater après edit, jamais de force-push, faire tourner le type-checker avant qu'un commit lande. Il n'y a rien à penser, alors ne fais pas penser le modèle.
Deux tests aident. Le premier : si casser la règle une fois casserait la production ou ouvrirait un trou de sécurité, c'est un hook ; si la casser une fois ne ferait qu'ouvrir une conversation de code review, c'est CLAUDE.md. Le deuxième est plus rapide. Si tu te surprends à taper ALWAYS ou NEVER en majuscules dans ton CLAUDE.md, la règle a probablement dépassé le prompt et veut être un hook. (Loader de la connaissance à la demande est un troisième job, séparé, et c'est à ça que servent les skills.)
Cette frontière, entre ce que tu prompt et ce que tu enforces, est la colonne vertébrale du module The Feedback Loop du cours d'agentic coding.
Le piège de l'autre côté
Voilà ce dont personne ne parle quand ils montrent leur setup de hooks : chaque hook fire une commande, et l'overhead se compose. Pre-check chaque appel d'outil, post-valider chaque write, faire tourner trois évaluations à chaque stop, et une session qui prenait deux minutes en prend dix. Je suis passé par cette phase. Le setup au-dessus, c'est ce à quoi je suis revenu.
La garantie déterministe coupe dans les deux sens. Un hook fait exactement ce que tu lui as dit, à chaque fois, y compris les fois où tu avais tort. Pendant que j'assemblais les notes pour cet article, un de mes propres hooks a bloqué la commande que je lançais, parce que le texte qu'il écrivait contenait git push --force comme exemple de ce qu'il faut attraper. Le matcher cherche cette string n'importe où dans une commande Bash, et il ne peut pas distinguer un vrai force-push d'un texte qui en parle. Le hook n'était pas cassé. Il faisait son boulot sans jugement, ce qui est tout le but, et, de temps en temps, tout le problème.
Donc ajoute un hook seulement quand tu l'as gagné : tu as corrigé la même erreur deux fois, tu as essayé de la fix dans CLAUDE.md, et elle est revenue. C'est ça, le signal. Pas la première fois que quelque chose tourne mal, et pas « ça serait sympa de l'enforce ».
Une note de placement, parce que ça mord les gens. Un hook qui garde une règle au niveau projet, comme le formatage ou ta liste de commandes dangereuses, va dans .claude/settings.json, committé, pour que l'agent de chaque teammate tourne les mêmes guardrails. Garde .claude/settings.local.json pour les trucs personnels : ton son de notification, un path d'outil local.
Arrête d'espérer, commence à câbler
Tu as déjà fait ce move une fois. Tu as arrêté de faire confiance au fait que tout le monde se souviendrait de lancer les tests, et tu les as câblés dans la CI. Les hooks, c'est la même décision un cran en dessous : arrêter de faire confiance à ce que l'agent se souvienne des règles qui ne survivent pas à une longue session, et câbler les quelques-unes qui ne peuvent vraiment pas être skippées dans le lifecycle lui-même.
Format on edit. Bloquer les trucs dangereux. Verify avant de stopper. Trois hooks, à peu près dix minutes, et les règles que CLAUDE.md droppe discrètement deviennent des choses que ton agent ne peut pas ignorer.
Les hooks sont une couche de la feedback loop qui fait qu'un setup agentic s'aiguise avec le temps. Le système complet est dans le cours d'agentic coding.
Apprenez le workflow agentic coding que j'utilise en production
Comment je structure mes repos, gère le contexte, et fais tourner des agents en production. Écrit pour que vous puissiez faire pareil.