Claude Code a 30 hooks. Voici ce que chacun fait.
Du PreToolUse qui bloque une commande dangereuse au WorktreeCreate qui isole un agent — la référence complète, événement par événement.
J'ai voulu empêcher Claude Code de toucher à mon .env. Réflexe classique : je demande gentiment dans le CLAUDE.md de ne pas y toucher. Ça marche neuf fois sur dix. La dixième, un Edit passe quand même, parce qu'un LLM suit des instructions, il ne les applique pas comme une règle de compilateur.
Les hooks règlent ce problème différemment : ce sont des commandes shell (ou des appels HTTP, ou des outils MCP) que Claude Code exécute lui-même, à des points précis de son cycle de vie. Pas de "please ne fais pas ça" — un script qui sort avec le code 2 et bloque l'action, point final. Même logique que ce qu'on cherche à obtenir avec l'intégration continue : remplacer un rappel humain par une règle qui s'exécute à chaque fois, sans exception.
Le vrai piège, c'est qu'il n'y a pas un seul hook générique "avant une action" et "après une action". Il y en a 30, chacun avec son propre déclencheur, son propre schéma JSON, et surtout ses propres règles de blocage — certains bloquent, d'autres non, et deux hooks qui se ressemblent (PreToolUse / PermissionRequest) ne se déclenchent pas dans les mêmes conditions. Ce guide les couvre un par un.
Anatomie d'un hook
Un hook se déclare dans un fichier de settings, sous la clé hooks. Chaque nom d'événement contient un tableau d'entrées, chacune avec un matcher (qui filtre quand le hook se déclenche) et un tableau hooks (les handlers à exécuter) :
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}Le matcher dépend de l'événement : nom d'outil pour PreToolUse, type de notification pour Notification, raison de compaction pour PreCompact. Une chaîne vide ("") fait matcher tout. Sur les événements outil, le matcher accepte aussi de vraies expressions régulières — ^Notebook ou mcp__.* sont valides, pas seulement des noms exacts.
Cinq types de handlers
Un hook n'est pas forcément une commande shell :
| Type | Rôle | Timeout par défaut |
|---|---|---|
command | Exécute un script shell, reçoit le JSON sur stdin | 600 s |
http | Envoie une requête POST à une URL | 600 s |
mcp_tool | Appelle un outil MCP déjà configuré | 600 s |
prompt | Demande à un modèle (Haiku par défaut) de trancher | 30 s |
agent | Lance un sous-agent avec accès outils pour évaluer une condition | 60 s |
Les trois derniers types existent pour les décisions qui demandent du jugement plutôt qu'une règle déterministe — un prompt hook peut évaluer "cette commande bash a-t-elle l'air dangereuse ?" sans que vous ayez à écrire une regex pour chaque cas.
Où déclarer un hook
| Emplacement | Portée | Versionné dans git |
|---|---|---|
~/.claude/settings.json | Toutes les sessions, cette machine | Non |
.claude/settings.json | Ce projet | Oui |
.claude/settings.local.json | Ce projet, cette machine | Non |
| Politique managée (admin org) | Toute l'organisation | N/A |
hooks/hooks.json d'un plugin | Tant que le plugin est actif | Oui |
Les administrateurs d'organisation peuvent activer allowManagedHooksOnly pour bloquer les hooks utilisateur, projet et plugin — seuls les hooks distribués via un marketplace interne survivent. Utile pour imposer des garde-fous de sécurité sans dépendre de la config locale de chaque développeur.
Le cycle de session
Trois événements encadrent la session elle-même.
| Hook | Se déclenche | Bloque ? |
|---|---|---|
SessionStart | Ouverture ou reprise d'une session (startup, resume, clear, compact, fork) | Non |
Setup | Lancement avec --init ou --maintenance | Non |
SessionEnd | Fin de session (clear, resume, logout, etc.) | Non |
Aucun des trois ne bloque quoi que ce soit — ce sont des points d'injection et de nettoyage, pas des portes. SessionStart est le plus utile en pratique : tout ce que le hook écrit sur stdout est ajouté au contexte de Claude. Avec le matcher compact, on peut réinjecter des rappels de conventions projet juste après une compaction, moment où Claude perd le plus de détails fins.
Le tour utilisateur
| Hook | Se déclenche | Bloque ? |
|---|---|---|
UserPromptSubmit | Vous soumettez un prompt, avant que Claude le traite | Oui — exit 2 efface le prompt |
UserPromptExpansion | Une commande slash s'étend en prompt, avant d'atteindre Claude | Oui — bloque l'expansion |
UserPromptSubmit a un timeout réduit à 30 secondes (contre 600 pour la plupart des autres hooks) : logique, personne n'a envie d'attendre 10 minutes avant que son prompt parte. C'est le point d'entrée idéal pour valider un format de prompt ou injecter du contexte dynamique (date du jour, ticket en cours) via additionalContext.
Les appels d'outils
C'est la famille la plus dense, et celle où les nuances de blocage comptent le plus.
| Hook | Se déclenche | Bloque ? |
|---|---|---|
PreToolUse | Avant qu'un outil s'exécute | Oui — permissionDecision: "deny" |
PermissionRequest | Quand la boîte de dialogue de permission s'affiche | Oui — behavior: "deny" |
PermissionDenied | Un outil est refusé par le classificateur auto | Non — mais peut proposer retry: true |
PostToolUse | Après qu'un outil a réussi | Oui — mais l'outil a déjà tourné |
PostToolUseFailure | Après qu'un outil a échoué | Oui — stoppe la boucle agentique |
PostToolBatch | Après un lot complet d'appels parallèles | Oui — avant le prochain appel modèle |
Deux confusions classiques :
PreToolUse n'est pas PermissionRequest. PreToolUse se déclenche systématiquement, dans tous les modes, avant toute vérification de permission — c'est le point pour imposer une politique que l'utilisateur ne peut pas contourner. PermissionRequest ne se déclenche qu'en mode interactif, quand la boîte de dialogue apparaît réellement ; c'est le bon endroit pour de l'auto-approbation ("toujours accepter ExitPlanMode"), pas pour de la sécurité.
PostToolUse ne peut pas annuler l'action. L'outil a déjà tourné. decision: "block" stoppe la boucle agentique et montre votre raison à Claude, mais ne défait rien. Pour empêcher une écriture de fichier, il faut agir en PreToolUse — PostToolUse sert à la vérification a posteriori (formatage, logs, feedback).
PreToolUse accepte aussi updatedInput : le hook peut réécrire les arguments de l'outil avant exécution, pas seulement les autoriser ou refuser.
Agents et tâches
| Hook | Se déclenche | Bloque ? |
|---|---|---|
SubagentStart | Un sous-agent est lancé | Non |
SubagentStop | Un sous-agent termine | Oui — empêche l'arrêt, le fait continuer |
TaskCreated | Une tâche est créée via TaskCreate | Oui — annule la création |
TaskCompleted | Une tâche est marquée terminée | Oui — empêche de la marquer terminée |
TeammateIdle | Un coéquipier d'agent team passe en idle | Oui — le force à continuer |
Le matcher de SubagentStart/SubagentStop accepte le type d'agent (general-purpose, Explore, Plan) mais aussi des noms scoppés par plugin sous forme de regex, comme ^my-plugin:reviewer$.
La fin de tour
| Hook | Se déclenche | Bloque ? |
|---|---|---|
Stop | Claude termine sa réponse | Oui — decision: "block" l'empêche de s'arrêter |
StopFailure | Le tour se termine sur une erreur API | Non — sortie et code ignorés |
Stop est le hook le plus intéressant de toute la liste pour qui veut imposer une discipline de fin de tâche : lancer les tests, vérifier un lint, confirmer qu'un TODO a bien été traité. additionalContext remonte comme un rappel système que Claude peut lire et sur lequel il peut agir — c'est ainsi qu'on le fait reprendre le travail sans lui dire frontalement "recommence".
StopFailure a un matcher sur le type d'erreur (rate_limit, overloaded, authentication_failed, etc.) mais reste purement informatif — utile pour logger, pas pour intervenir.
Fichiers et environnement
| Hook | Se déclenche | Bloque ? |
|---|---|---|
FileChanged | Un fichier surveillé change sur disque | Non |
CwdChanged | Le répertoire de travail change (cd) | Non |
InstructionsLoaded | Un CLAUDE.md ou .claude/rules/*.md est chargé | Non |
Le matcher de FileChanged n'accepte que des noms de fichiers littéraux séparés par | (.envrc|.env) — pas de regex, pas de wildcard. C'est volontaire : on surveille des fichiers précis, pas des motifs.
Configuration
| Hook | Se déclenche | Bloque ? |
|---|---|---|
ConfigChange | Un fichier de config change pendant la session | Oui, sauf pour policy_settings |
Le matcher distingue user_settings, project_settings, local_settings, policy_settings et skills. Utile pour un audit de conformité : qui a modifié quoi, et quand — sans pouvoir bloquer les décisions de la politique managée, logiquement, puisque c'est justement le garde-fou du dessus.
Compaction
| Hook | Se déclenche | Bloque ? |
|---|---|---|
PreCompact | Avant la compaction du contexte | Oui |
PostCompact | Après la compaction | Non |
Matcher commun aux deux : manual ou auto, selon que c'est vous qui avez tapé /compact ou que la fenêtre de contexte a débordé.
Worktrees
| Hook | Se déclenche | Bloque ? |
|---|---|---|
WorktreeCreate | Création d'un worktree (--worktree, isolation, session en arrière-plan) | Oui — tout code de sortie non nul échoue la création |
WorktreeRemove | Suppression d'un worktree | Non |
Particularité de WorktreeCreate : un hook command doit imprimer le chemin du worktree sur stdout pour que la création réussisse. C'est le seul hook de toute la liste où le contrat de sortie est aussi strict.
Notifications et affichage
| Hook | Se déclenche | Bloque ? |
|---|---|---|
Notification | Claude Code envoie une notification | Non — fire-and-forget |
MessageDisplay | Pendant que le texte de la réponse s'affiche | Non — affichage seul |
Notification a huit valeurs de matcher possibles : permission_prompt, idle_prompt, auth_success, elicitation_dialog, elicitation_complete, elicitation_response, agent_needs_input, agent_completed. MessageDisplay a un timeout réduit à 10 secondes — logique, il tourne pendant que le texte défile à l'écran, il ne doit pas ralentir la lecture.
Élicitation MCP
| Hook | Se déclenche | Bloque ? |
|---|---|---|
Elicitation | Un serveur MCP demande une saisie utilisateur | Oui — action: "decline" ou "cancel" |
ElicitationResult | Après la réponse utilisateur, avant l'envoi au serveur | Oui — peut transformer la réponse en decline |
Ces deux hooks concernent uniquement les serveurs MCP qui ouvrent des formulaires interactifs (elicitation dans le protocole MCP) — une fonctionnalité encore peu utilisée, mais qui permet d'auto-répondre ou de filtrer ce qui remonte à un serveur externe.
Ce qui bloque vraiment, et comment
Un point qui coûte cher si on le rate, surtout si vos hooks servent à faire respecter des règles de sécurité côté développeur : seul le code de sortie 2 bloque une action, pour la quasi-totalité des hooks. Le code 1, pourtant code d'échec Unix classique, est traité comme une erreur non bloquante — l'action continue. Si votre hook doit faire respecter une politique, exit 2 est non négociable.
Même un JSON invalide sur stdout avec exit 2 bloque quand même : Claude Code retombe sur stderr comme raison de blocage, et journalise l'échec de validation en debug. Le blocage est prioritaire sur la propreté du format.
Pour cibler un hook plus finement qu'un simple nom d'outil, le champ if accepte la même syntaxe que les règles de permission :
{
"type": "command",
"if": "Bash(git push*)",
"command": "/path/to/check-branch.sh"
}Ce champ n'est évalué que sur les événements liés aux outils (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied) — ailleurs, un hook avec if défini ne se déclenche jamais. Le matching est plus malin qu'un simple startswith : Bash(git *) matche FOO=bar git push (les affectations en tête sont ignorées) et npm test && git push (chaque sous-commande est vérifiée séparément), y compris à l'intérieur de $() ou de backticks.
Autre détail qui a une vraie incidence en sécurité : depuis la v2.1.139, les hooks command tournent sans terminal de contrôle sur macOS et Linux. Ni le hook ni ses processus enfants ne peuvent ouvrir /dev/tty ni écrire directement dans l'interface — d'où l'intérêt du champ terminalSequence en sortie JSON plutôt que d'essayer d'imprimer quelque chose à l'écran.
Côté variables d'environnement, chaque hook reçoit $CLAUDE_PROJECT_DIR (racine du projet), et pour les hooks de plugin $CLAUDE_PLUGIN_ROOT et $CLAUDE_PLUGIN_DATA. $CLAUDE_EFFORT expose le niveau d'effort actif. Une chose qu'ils ne reçoivent jamais : les variables OTEL_* d'export de télémétrie, retirées systématiquement de tout sous-processus pour éviter les doublons de reporting.
Trente événements, cinq types de handlers, un champ if avec sa propre grammaire — la surface est large, mais la logique est cohérente une fois qu'on a repéré les familles. La suite part de cette référence pour construire des automatisations concrètes, catégorie par catégorie.