Ce que les hooks de Claude Code changent une fois qu'on les utilise vraiment
Formatage automatique, garde-fous de sécurité, notifications, contexte persistant — une recette concrète pour chaque famille de hooks.
Le guide complet des 30 hooks répond à "qu'est-ce que ça fait". Celui-ci répond à "qu'est-ce que j'en fais". Même organisation par famille, mais cette fois avec des scripts qu'on peut coller dans un .claude/settings.json et tester dans la minute.
Cycle de session
SessionStart est le meilleur endroit pour réinjecter du contexte qu'un CLAUDE.md statique ne peut pas fournir — parce que ce que le hook écrit sur stdout dépend de l'instant présent, pas d'un fichier figé :
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{ "type": "command", "command": "git log --oneline -5" }
]
}
]
}
}Après chaque compaction, Claude revoit les cinq derniers commits au lieu de redemander "sur quoi on travaillait déjà ?". Avec le matcher startup à la place de compact, la même idée sert à rappeler l'état d'un ticket Jira en cours au tout début de session.
Setup se déclenche seulement avec --init ou --maintenance — utile pour un script de provisioning : installer les dépendances d'un template, vérifier qu'un .env existe avant de laisser Claude travailler dessus.
SessionEnd ne bloque rien, mais c'est un bon endroit pour purger un fichier temporaire ou fermer une connexion ouverte par un SessionStart symétrique (tunnel SSH, base de données de test).
Tour utilisateur
UserPromptSubmit peut refuser un prompt avant qu'il parte — pratique pour imposer une convention d'équipe, comme exiger qu'un prompt commençant par "deploy" mentionne explicitement l'environnement cible :
#!/bin/bash
INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.user_input')
if [[ "$PROMPT" == deploy* ]] && [[ "$PROMPT" != *"staging"* ]] && [[ "$PROMPT" != *"production"* ]]; then
echo "Précise l'environnement : staging ou production." >&2
exit 2
fiUserPromptExpansion intercepte l'expansion d'une commande slash avant qu'elle atteigne Claude — on peut journaliser quelles commandes personnalisées sont réellement utilisées dans l'équipe, ou bloquer une commande dépréciée en redirigeant vers son remplacement dans le message d'erreur.
Appels d'outils
C'est la famille la plus riche, et celle qui mérite le plus de code.
PreToolUse bloque avant exécution. L'exemple classique : interdire les fichiers sensibles.
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
for pattern in ".env" "package-lock.json" ".git/"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Bloqué : $FILE_PATH correspond au motif protégé '$pattern'" >&2
exit 2
fi
done
exit 0Avec le champ if, on peut scoper le hook à un sous-ensemble précis de commandes Bash plutôt qu'à tout l'outil — par exemple ne vérifier que les git push :
{
"type": "command",
"if": "Bash(git push*)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-branch.sh"
}Le script peut alors refuser un push direct sur main sans jamais interférer avec les autres commandes bash.
PermissionRequest n'est pas pour la sécurité — c'est pour le confort. Auto-approuver les prompts qu'on accepte systématiquement :
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}'"
}
]
}
]
}
}PermissionDenied répare les faux positifs du mode auto : quand le classificateur refuse un outil légitime, ce hook peut renvoyer retry: true pour laisser Claude retenter, au lieu de bloquer une action pourtant sûre.
PostToolUse formate automatiquement — l'exemple le plus copié de toute la documentation :
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}PostToolUseFailure capture les échecs pour construire un historique de flakiness — logger chaque commande bash qui échoue, avec son message d'erreur, dans un fichier qu'on relit en fin de semaine pour repérer les outils qui plantent souvent pour de mauvaises raisons.
PostToolBatch se déclenche après un lot entier d'appels parallèles. C'est le bon endroit pour une vérification globale — par exemple confirmer qu'aucun fichier du lot n'a introduit de console.log oublié, sans avoir à le faire outil par outil.
Agents et tâches
SubagentStart / SubagentStop permettent de tracer ce que font réellement les sous-agents qu'on lance — utile dès qu'un projet utilise beaucoup de délégation. Un hook simple sur SubagentStop qui écrit agent_type, agent_id et la durée dans un log donne une vraie visibilité sur qui fait quoi, sans avoir à relire chaque transcript.
TaskCreated / TaskCompleted se prêtent à la synchronisation avec un outil externe. Avec un hook http, chaque tâche créée par Claude peut pousser une entrée dans un tracker :
{
"hooks": {
"TaskCompleted": [
{
"hooks": [
{
"type": "http",
"url": "https://hooks.slack.com/services/xxx",
"headers": { "Content-Type": "application/json" }
}
]
}
]
}
}Chaque tâche terminée arrive dans un canal Slack, sans script intermédiaire.
TeammateIdle ne concerne que les agent teams — un coéquipier sur le point de passer en idle. On peut l'utiliser pour lui faire vérifier une checklist ("tous les tests passent-ils avant de s'arrêter ?") avant de le laisser vraiment inactif.
Fin de tour
Stop est le hook le plus payant à configurer sérieusement : imposer qu'une suite de tests passe avant que Claude déclare une tâche terminée.
#!/bin/bash
# .claude/hooks/verify-tests.sh
if ! npm test --silent > /tmp/test-output.log 2>&1; then
REASON=$(tail -20 /tmp/test-output.log)
echo "{\"decision\": \"block\", \"reason\": \"Tests en échec :\n$REASON\"}"
exit 0
fiCe n'est pas différent, dans l'esprit, d'un test end-to-end avec Playwright qui refuse de laisser passer un déploiement cassé — sauf qu'ici la vérification tourne à chaque fin de tour, pas seulement en CI.
StopFailure n'intervient pas sur le flux, mais journaliser le error_type (rate_limit, overloaded, authentication_failed...) donne un historique utile pour savoir si les coupures viennent d'un problème de quota ou d'un vrai souci d'auth.
Fichiers et environnement
FileChanged + CwdChanged couplés à SessionStart résolvent un vrai angle mort : Bash dans Claude Code ne recharge pas automatiquement les variables d'environnement gérées par direnv.
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "direnv export bash > \"$CLAUDE_ENV_FILE\"" }] }
],
"CwdChanged": [
{ "hooks": [{ "type": "command", "command": "direnv export bash > \"$CLAUDE_ENV_FILE\"" }] }
]
}
}CLAUDE_ENV_FILE est rejoué comme préambule avant chaque commande Bash — donc dès que Claude fait un cd vers un dossier avec son propre .envrc, les variables suivent.
InstructionsLoaded est purement informatif, mais journaliser quel CLAUDE.md ou fichier de règles est chargé, et pourquoi (session_start, nested_traversal, include...), aide à déboguer les cas où un rappel de convention n'arrive pas jusqu'à Claude dans un monorepo à plusieurs CLAUDE.md imbriqués.
Configuration
ConfigChange journalise toute modification des fichiers de settings pendant la session :
{
"hooks": {
"ConfigChange": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log" }
]
}
]
}
}Sur une équipe qui traite ça comme du code sensible en termes de sécurité, c'est un début de piste d'audit sans outillage lourd.
Compaction
PreCompact peut bloquer une compaction automatique si un état critique n'a pas encore été sauvegardé — par exemple si un git status révèle des fichiers non commités que vous préférez voir traités avant que le contexte ne soit résumé.
PostCompact est le complément naturel de SessionStart(compact) vu plus haut : les deux se déclenchent au même instant fonctionnel, mais PostCompact a accès à la raison de compaction sans avoir à la redéduire.
Worktrees
WorktreeCreate peut rediriger la création vers un chemin personnalisé — le hook doit imprimer ce chemin sur stdout pour que ça marche, contrat plus strict que les autres hooks. Utile pour forcer tous les worktrees d'un projet dans un répertoire dédié plutôt qu'à côté du repo.
WorktreeRemove est l'endroit pour nettoyer ce que WorktreeCreate a mis en place : arrêter un service lancé spécifiquement pour ce worktree, purger un node_modules volumineux avant suppression.
Notifications et affichage
Notification est le hook qu'on configure en premier, tellement il est immédiat :
{
"hooks": {
"Notification": [
{
"matcher": "idle_prompt",
"hooks": [
{ "type": "command", "command": "notify-send 'Claude Code' 'En attente de votre prochain prompt'" }
]
}
]
}
}Avec le matcher agent_needs_input, la même notification couvre les sessions en arrière-plan qui attendent une réponse — pratique pour ne pas laisser un agent bloqué toute une après-midi sans s'en rendre compte.
MessageDisplay est plus expérimental : il tourne pendant que le texte s'affiche, avec un timeout de 10 secondes seulement. Un usage raisonnable : masquer ou reformater à la volée un motif sensible (une clé d'API qui apparaîtrait par erreur dans une réponse) avant qu'il ne s'affiche complètement à l'écran.
Élicitation MCP
Elicitation et ElicitationResult ne concernent que les serveurs MCP avec formulaires interactifs. Un cas concret : un serveur MCP de déploiement demande confirmation avant une action destructive. Un hook Elicitation peut journaliser chaque formulaire ouvert, avec le nom du serveur et de l'outil, avant même que vous n'y répondiez — utile pour repérer quels serveurs MCP demandent le plus souvent une confirmation humaine, et donc lesquels mériteraient une politique plus stricte en amont via PreToolUse.
Aucune de ces recettes ne demande plus de dix lignes de shell ou un JSON de plus de quinze lignes. Le point commun, c'est de choisir le bon événement — pas le plus proche dans le temps de ce qu'on veut empêcher, mais celui qui a réellement le pouvoir de bloquer. Direction le guide complet pour vérifier lequel avant d'écrire le script.