Définir un hook
Sur cette page
Créer un hook revient à répondre à quatre questions, dans l’ordre. Quel événement : PreToolUse si votre code doit pouvoir empêcher l’action, PostToolUse s’il réagit à une action faite, un autre événement pour le reste du cycle de vie. Quels outils : le matcher, et au besoin la condition if, précisent ce qui déclenche le hook. Quelle commande : un script qui lit les données de l’événement en JSON sur l’entrée standard. Quel verdict : le code de sortie, ou un objet JSON sur la sortie standard, dit à Claude Code s’il doit continuer. Un hook n’est donc pas un programme compliqué : il lit du JSON, décide, et sort avec le bon code.
Choisir les outils : matcher et if
Le champ matcher filtre les appels. Un nom seul (Bash) correspond exactement à cet outil ; plusieurs noms séparés par | ou par une virgule (Edit|Write) à l’un d’eux ; tout autre caractère fait du matcher une expression régulière JavaScript non ancrée (^Notebook, mcp__memory__.*). La casse compte : bash ne correspond pas à Bash. Vide, * ou absent, le matcher accepte tout.
Pour affiner sur les arguments, le champ if accepte une règle de permission, une seule par hook : "Bash(rm *)", "Edit(*.ts)", "Edit(**/src/**)". Le script n’est lancé que si l’appel correspond, ce qui évite de démarrer un processus pour chaque commande. Il n’est évalué que sur les événements d’outils.
Pour savoir quels outils existent dans votre session, demandez à Claude : « Quels outils as-tu à ta disposition ? » La liste change quand vous ajoutez des serveurs MCP (leurs outils s’appellent mcp__<serveur>__<outil>), et un hook écrit il y a trois mois peut être devenu incomplet le jour où un serveur a apporté sa propre manière d’accéder au disque.
Les données reçues
Quand votre commande s’exécute, Claude Code lui envoie du JSON sur stdin. Tous les événements partagent des champs communs ; les événements d’outils ajoutent le nom de l’outil et ses arguments :
{
"session_id": "2d6a1e4d-6…",
"transcript_path": "/home/vous/.claude/projects/…/2d6a1e4d.jsonl",
"cwd": "/code/projet",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "/code/projet/.env"
},
"tool_use_id": "toolu_01ABC…"
}
hook_event_name rappelle où vous êtes dans le cycle, tool_name identifie l’outil, tool_input contient ses arguments : c’est là que se loge l’information sur laquelle vous statuez. Les champs de tool_input dépendent de l’outil : command pour Bash, file_path pour Read, Edit et Write (avec old_string et new_string pour Edit, content pour Write), pattern et path pour Grep. Deux garanties facilitent les comparaisons : les chemins de fichiers arrivent toujours absolus, ~ et les chemins relatifs étant développés avant les hooks, et sous Windows ils arrivent avec des barres obliques inverses, qu’il faut normaliser avant de comparer. Un PostToolUse reçoit en plus tool_response, le résultat renvoyé par l’outil, et duration_ms.
Le verdict : codes de sortie
Le dialogue en retour est minimaliste. Votre script sort, et son code de sortie tient lieu de verdict :
| Code | Signification |
|---|---|
| 0 | Aucune objection : l’appel suit le flux de permission normal (exit 0 n’approuve pas, il ne bloque pas) |
| 2 | Bloquer l’action ; ce que le script a écrit sur stderr est transmis à Claude comme explication |
| Autre | Erreur non bloquante : l’action continue, un avis hook error s’affiche dans la conversation |
Le message sur stderr change beaucoup de choses. Un blocage muet laisse Claude devant un échec inexpliqué, qu’il tentera de contourner, parfois par un chemin moins souhaitable. Un blocage qui dit « fichier protégé, utilise la variable d’environnement à la place » lui donne de quoi ajuster son plan.
Le code 2 ne bloque que sur les événements qui représentent une action pas encore faite : PreToolUse (l’appel), UserPromptSubmit (le prompt), Stop (Claude est empêché de s’arrêter et continue), PreCompact, PreModelSwitch. Sur PostToolUse, l’outil a déjà tourné : le code 2 montre seulement votre stderr à Claude, ce qui reste utile pour lui signaler une erreur de type ou un test cassé. Sur Notification ou SessionEnd, il est ignoré.
Attention à une faille classique : si votre hook plante, n’existe pas ou dépasse son délai, l’action continue. Un hook de sécurité au chemin erroné laisse tout passer. Pour qu’un échec bloque, ajoutez "onFailure": "block" au hook (version 2.1.295 et au-delà).
Le verdict : sortie JSON
Les codes de sortie ne permettent que de bloquer ou de se taire. Pour un contrôle plus fin, sortez avec le code 0 et écrivez un objet JSON, seul, sur stdout. Sur PreToolUse :
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Utilise rg plutôt que grep sur ce dépôt"
}
}
permissionDecision vaut deny (annule l’appel et transmet la raison à Claude), ask (affiche la demande de permission normale), allow (saute la demande interactive ; les règles deny des réglages continuent de s’appliquer) ou defer en mode non interactif. Le même objet accepte updatedInput pour réécrire les arguments de l’outil avant qu’il ne tourne, et additionalContext pour glisser une information à Claude. Choisissez une approche par hook, codes de sortie ou JSON : si vous les mélangez, le code 2 garde son effet bloquant. Le JSON doit être la seule chose sur stdout ; une ligne que votre profil de shell affiche au démarrage suffit à le rendre illisible.
Cas d’utilisation courant
Le cas classique est d’empêcher Claude de lire des fichiers sensibles comme .env. L’exercice semble trivial jusqu’au moment où l’on réalise qu’il existe plusieurs portes d’entrée : l’outil Read, mais aussi Grep là où il existe, et surtout cat .env par l’outil Bash. Avant d’écrire un hook de protection, demandez-vous par quels outils la même information pourrait sortir, et couvrez-les tous. Pour ce cas précis, Claude Code offre d’ailleurs une solution intégrée que la leçon suivante compare au hook : une règle deny Read(./.env), qui couvre aussi Edit, Write et les commandes de lecture reconnues dans Bash. La leçon suivante implémente le hook, de la configuration au test.
À vous de jouer
- Écrivez un hook
PreToolUsesurBashavec"if": "Bash(git push *)"dont la commande estexit 2précédé d’unecho "Pas de push depuis Claude Code" >&2. Demandez à Claude de pousser la branche : lisez le message qu’il reçoit et ce qu’il propose à la place. - Remplacez
exit 2par une sortie JSONpermissionDecision: "ask"et observez la différence. - Mettez volontairement un chemin de script faux dans un hook
PreToolUseet constatez que l’action passe quand même ; ajoutez"onFailure": "block"et recommencez.
Sources officielles : Hooks reference · Automate actions with hooks · Tools reference
Points clés à retenir
- Quatre questions : quel événement, quels outils (
matcher,if), quelle commande, quel verdict - Le hook lit du JSON sur stdin :
hook_event_name,tool_name,tool_input(chemins absolus, barres inverses sous Windows) - Exit 0 = pas d’objection, exit 2 = blocage avec stderr comme explication ; le code 2 ne bloque que les actions pas encore faites
- Pour plus de finesse, exit 0 et un objet JSON seul sur stdout :
permissionDecisionallow, deny, ask ;updatedInput;additionalContext - Un hook qui plante laisse passer l’action, sauf avec
"onFailure": "block"