Aller au contenu principal
Leçon 13 sur 20 · Claude Code en action

Définir un hook

Leçon 13 sur 20 Mis à jour le 4 novembre 2026 Développeur
Sur cette page
  1. Choisir les outils : matcher et if
  2. Les données reçues
  3. Le verdict : codes de sortie
  4. Le verdict : sortie JSON
  5. Cas d’utilisation courant
  6. À vous de jouer
  7. Points clés à retenir

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 :

CodeSignification
0Aucune objection : l’appel suit le flux de permission normal (exit 0 n’approuve pas, il ne bloque pas)
2Bloquer l’action ; ce que le script a écrit sur stderr est transmis à Claude comme explication
AutreErreur 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

  1. Écrivez un hook PreToolUse sur Bash avec "if": "Bash(git push *)" dont la commande est exit 2 précédé d’un echo "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.
  2. Remplacez exit 2 par une sortie JSON permissionDecision: "ask" et observez la différence.
  3. Mettez volontairement un chemin de script faux dans un hook PreToolUse et 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 : permissionDecision allow, deny, ask ; updatedInput ; additionalContext
  • Un hook qui plante laisse passer l’action, sauf avec "onFailure": "block"