Hooks Claude Code : transformer les règles de l'agent en garanties
Les hooks Claude Code sont des commandes shell que Claude Code lance lui-même à certains événements : avant et après l'appel d'un outil, à l'envoi d'un prompt, au démarrage de la session et avant l'arrêt de l'agent. Ils se configurent dans settings.json, et un code de sortie 2 bloque l'action en renvoyant un message d'erreur à l'agent. Le hook transforme ainsi une règle du CLAUDE.md en garantie que le modèle ne peut pas oublier.
Ce que sont les hooks de Claude Code
Un hook est une commande lancée non pas par le modèle, mais par Claude Code lui-même à un moment précis du travail de l'agent. L'agent s'apprête à exécuter rm -rf — un hook se déclenche et décide de laisser passer ou non la commande. L'agent a modifié un fichier — un hook lance le formateur. L'agent veut terminer — un hook exécute les tests et empêche l'arrêt tant qu'ils sont rouges.
La différence majeure avec les instructions du CLAUDE.md : une instruction est un texte que le modèle prend en compte avec une certaine probabilité, alors qu'un hook est du code déterministe. Il se déclenche toujours, quelle que soit la longueur du contexte, quoi que l'agent ait « décidé » et qu'il ait ou non oublié la règle à la centième minute de la session. C'est pourquoi les hooks de Claude Code sont le moyen principal de transformer un souhait en garantie.
Pourquoi les règles dans CLAUDE.md ne suffisent pas
Scène typique : l'agent a terminé une fonctionnalité, les tests unitaires sont verts, le chat affiche un « C'est terminé » assuré. Or personne n'a lancé le scénario de bout en bout, le linter n'a pas tourné, et une migration a échoué au milieu. Pourtant, le CLAUDE.md dit : « avant de finir, lance les tests ». Le modèle ne trompe pas délibérément : il s'évalue lui-même, et cette auto-évaluation n'est pas fiable.
D'où le principe du harnais : tout ce qui peut être vérifié mécaniquement est vérifié par le mécanisme, et non par le modèle. La règle « lance le linter » devient un hook qui lance le linter. La règle « ne touche pas à la configuration de production » devient un hook qui bloque l'écriture dans ce fichier. L'instruction textuelle reste, mais elle n'explique plus qu'à l'agent pourquoi il a été arrêté.
Les événements : quand les hooks se déclenchent
Chaque hook est rattaché à un événement du cycle de vie de la session. Les événements principaux :
| Événement | Quand il se déclenche | Usage typique |
|---|---|---|
PreToolUse | avant l'appel d'un outil (Bash, Edit, Write, etc.) | interdire les commandes dangereuses, protéger des fichiers |
PostToolUse | après un appel d'outil réussi | formateur, linter, tests rapides après une modification |
UserPromptSubmit | quand l'utilisateur a envoyé un prompt, avant son traitement | ajouter du contexte, rejeter un prompt contenant un secret |
Stop | quand l'agent s'apprête à terminer sa réponse | empêcher l'arrêt tant que les contrôles ne sont pas passés |
SubagentStop | quand un subagent termine son travail | contrôle du résultat du subagent |
SessionStart | au démarrage ou à la reprise de la session | charger l'état : git status, plan, journal de progression |
SessionEnd | à la fin de la session | nettoyage, écriture des journaux |
Notification | quand Claude Code envoie une notification (par exemple, il attend une autorisation) | son ou message sur le téléphone |
PreCompact | avant la compaction du contexte | sauvegarder l'essentiel avant la compression |
Les plus utiles pour la fiabilité sont PreToolUse, PostToolUse et Stop : ils couvrent trois faiblesses de l'agent — les actions dangereuses, les modifications non vérifiées et le « c'est fini » prématuré.
Où se configurent les hooks : settings.json
Les hooks se décrivent dans les fichiers de configuration de Claude Code :
~/.claude/settings.json— vos réglages personnels, pour tous les projets ;.claude/settings.jsondans le dépôt — partagés par l'équipe, versionnés dans git ;.claude/settings.local.json— réglages locaux du projet, absents de git.
La structure est la suivante : événement → liste de groupes avec un matcher → liste de commandes.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
]
}
}
matcher est le nom d'un outil ou une expression régulière sur ce nom : Bash, Edit|Write, .*. Il sert pour les événements liés aux outils ; pour Stop ou UserPromptSubmit, il peut être omis. La variable CLAUDE_PROJECT_DIR pointe vers la racine du projet : il est donc pratique de ranger les scripts dans .claude/hooks/ et de les appeler par cette variable. Pour voir les hooks actifs, utilisez la commande /hooks dans Claude Code.
Comment un hook reçoit les données et répond
Entrée : JSON sur stdin
Claude Code transmet au hook un JSON décrivant l'événement : identifiant de session, répertoire de travail, nom de l'événement et, pour les outils, tool_name et tool_input. Pour Bash, la commande se trouve dans tool_input.command ; pour Edit et Write, le chemin du fichier se trouve dans tool_input.file_path. Le plus simple est de lire ce JSON avec jq.
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf build/" },
"cwd": "/home/dev/project"
}
Sortie : code de retour
| Code | Ce qui se passe |
|---|---|
0 | succès, le travail continue ; pour UserPromptSubmit et SessionStart, la sortie stdout est ajoutée au contexte de l'agent |
2 | erreur bloquante : stderr est transmis à l'agent et l'action est arrêtée (pour PreToolUse, l'appel d'outil est annulé ; pour Stop, l'agent poursuit son travail) |
| autre | erreur non bloquante : stderr est affiché à l'utilisateur, le travail continue |
Le code 2 est le mécanisme clé. L'agent reçoit le texte de stderr comme retour et peut se corriger : réécrire la commande, réparer le test, ne pas toucher au fichier. Écrivez donc dans stderr non pas « erreur », mais la cause claire et ce qu'il faut faire à la place.
En plus du code de retour, le hook peut écrire dans stdout un JSON contenant une décision, par exemple "decision": "block" et "reason". C'est utile pour un réglage fin, mais pour la plupart des cas, le code 2 et un message dans stderr suffisent.
Exemple 1 : linter et tests après chaque modification
Un hook PostToolUse avec le matcher Edit|Write se déclenche après la modification d'un fichier. L'outil a déjà été exécuté, la modification ne peut plus être annulée, mais le code 2 renvoie à l'agent la sortie du linter, et il corrige l'erreur immédiatement, et non une demi-heure plus tard.
#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
if ! out=$(ruff check "$file" 2>&1); then
echo "Le linter a trouvé des erreurs dans $file :" >&2
echo "$out" >&2
exit 2
fi ;;
*.php)
if ! out=$(php -l "$file" 2>&1); then
echo "$out" >&2
exit 2
fi ;;
esac
exit 0
N'y mettez pas une suite complète de tests de dix minutes : le hook se déclenche à chaque modification. Après une modification, lancez des contrôles rapides (syntaxe, linter, tests d'un seul module), et réservez les contrôles lourds au hook Stop.
Exemple 2 : interdire les commandes dangereuses
Un hook PreToolUse avec le matcher Bash voit la commande avant son exécution. Le code 2 annule l'appel, et l'agent reçoit une explication.
#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
echo "Commande bloquée par la politique du projet : $cmd. Les opérations dangereuses sont réalisées par un humain." >&2
exit 2
fi
exit 0
On protège de la même façon les fichiers : un hook sur Edit|Write vérifie file_path et bloque l'écriture dans .env, dans les configurations de production, dans le code généré ou dans les migrations déjà appliquées. Attention : une recherche par expression régulière peut être contournée — l'agent peut écrire la commande autrement. Le hook est une protection solide, mais il ne remplace ni les droits d'accès ni le bac à sable.
Exemple 3 : l'agent ne peut pas dire « c'est fini » tant que les tests sont rouges
Le hook Stop se déclenche quand l'agent s'apprête à terminer. Si les tests échouent, le code 2 empêche l'arrêt, et la sortie des tests part vers l'agent comme tâche suivante. C'est la version mécanique de la Definition of Done.
#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# protection contre la boucle infinie : le hook a déjà prolongé le travail une fois
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! out=$(npm test --silent 2>&1); then
echo "Les tests ne passent pas, le travail n'est pas terminé :" >&2
echo "$out" | tail -n 40 >&2
exit 2
fi
exit 0
Le champ stop_hook_active vaut true si l'agent poursuit déjà son travail à cause d'un hook d'arrêt. Sans cette vérification, le hook peut faire tourner l'agent en rond sans fin — c'est précisément la situation où l'agent « travaille toute la nuit dans la mauvaise direction ». Dans les boucles autonomes, ajoutez aussi une limite de tentatives et une condition qui appelle un humain.
Exemple 4 : charger l'état au début de la session
Au moment SessionStart, la sortie stdout du hook entre dans le contexte. Il est pratique d'afficher la branche courante, les modifications non commitées et la prochaine tâche du plan, pour que l'agent ne reparte pas de zéro.
#!/usr/bin/env bash
echo "Branche : $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0
Comment les hooks transforment les règles en garanties
Il est utile de répartir les règles du harnais en trois niveaux. Le premier est le texte du CLAUDE.md : il explique comment travailler, mais il est respecté de façon probabiliste. Le deuxième est constitué des contrôles exécutables : tests, tests de bout en bout, règles d'architecture sous forme de script qui échoue en cas de violation. Le troisième est constitué des hooks qui lancent automatiquement ces contrôles au bon moment, sans intervention du modèle.
Règle pratique : si l'agent a enfreint la même règle deux fois, n'ajoutez pas dans le CLAUDE.md un avertissement de plus en majuscules. Demandez-vous si la violation peut être vérifiée par un script. Si oui, écrivez ce script et accrochez-le à un hook. L'instruction textuelle devient alors plus courte, et le comportement plus stable.
| Règle écrite | Garantie par un hook |
|---|---|
| « Formate le code selon le standard » | PostToolUse lance le formateur sur le fichier modifié |
| « Ne fais pas de push avec --force » | PreToolUse sur Bash bloque la commande, exit 2 |
| « Ne dis pas que c'est fini tant que les tests sont rouges » | Stop lance les tests et empêche la fin |
| « Ne modifie pas .env » | PreToolUse sur Edit|Write vérifie le chemin |
| « Rappelle-toi où tu t'es arrêté » | SessionStart affiche le plan et git status |
Erreurs fréquentes
- Code 1 au lieu de 2. Le script échoue avec le code 1, l'agent ne l'apprend pas, et l'action passe. Seul le code 2 bloque.
- Message dans stdout. Avec le code 2, l'agent reçoit stderr. Écrivez avec
>&2. - Hook lent sur un événement fréquent. Une suite complète de tests dans PostToolUse rend chaque modification longue d'une minute. Mettez les contrôles lourds dans Stop.
- Stop sans fin. Pas de vérification de
stop_hook_activeni de limite — l'agent tourne sans fin. - Chemins relatifs. Le hook est lancé dans le répertoire de travail de la session ; utilisez
$CLAUDE_PROJECT_DIR. - Hook d'un tiers non vérifié. Le hook s'exécute avec vos droits, sans confirmation. Lisez les scripts provenant de dépôts tiers avant de leur faire confiance.
En résumé
Les hooks de Claude Code font passer les règles les plus importantes du texte au code : PreToolUse arrête les actions dangereuses, PostToolUse vérifie chaque modification, Stop empêche de déclarer le travail terminé trop tôt, et SessionStart restitue à l'agent la mémoire du projet. Commencez par deux ou trois hooks pour les règles que l'agent a déjà enfreintes, et formulez les messages dans stderr de façon claire : l'agent corrigera alors lui-même ses erreurs, et vous contrôlerez le résultat plutôt que le processus. La validation, le contrôle de la fin de tâche et les boucles autonomes sont traités en pratique dans le cours.
Questions fréquentes
Faut-il redémarrer Claude Code après avoir modifié les hooks ?
Les modifications de settings.json s'appliquent de façon plus fiable dans une nouvelle session. La configuration active peut être consultée avec la commande /hooks.
Peut-on écrire des hooks autrement qu'en bash ?
Oui, un hook est n'importe quelle commande : script Python, Node.js ou binaire. Il faut seulement lire le JSON sur stdin et renvoyer le bon code de sortie.
L'agent voit-il la sortie d'un hook qui réussit ?
En général non : avec le code 0, la sortie n'entre pas dans le contexte. Les exceptions sont UserPromptSubmit et SessionStart, où stdout est ajouté au contexte de l'agent.
Les hooks s'appliquent-ils aux subagents ?
Oui, les appels d'outils des subagents passent par les mêmes hooks PreToolUse et PostToolUse, et leur fin dispose d'un événement dédié, SubagentStop.
Est-il sûr de versionner les hooks dans le dépôt ?
Oui : .claude/settings.json et les scripts dans .claude/hooks/ sont une pratique courante pour une équipe. Gardez vos hooks personnels dans settings.local.json ou dans ~/.claude/settings.json.
Ce sujet est traité en détail avec de la pratique dans le cours « Harness Engineering: инженерия надёжности AI-агентов ».
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента
- Автономный AI-агент часами без присмотра: Loop Engineering и условия остановки