Claude Code hooks : exemples concrets (PostToolUse, Stop, PreToolUse)

Pour la dixième fois de la journée, je tape « n'oublie pas de lancer gofmt » à Claude Code. Puis « relance les tests avant de t'arrêter ». Puis « ne touche pas au .env ». Le jour où j'ai branché trois hooks, ces trois phrases ont disparu de mon vocabulaire, la machine les applique toute seule, à chaque fois, sans que j'y pense.

Les hooks Claude Code sont mal documentés en exemples réels : la doc liste les événements, mais pas les configs qui servent au quotidien. Voici les miennes, prêtes à copier, avec les pièges que j'ai pris au passage, dont la boucle infinie du Stop hook, le grand classique.

Un hook, c'est un script branché sur un événement

Un hook est une commande shell que Claude Code exécute automatiquement à un moment précis de son cycle. Il reçoit un JSON sur stdin (le nom de l'outil, ses arguments, le répertoire…) et peut soit laisser passer, soit bloquer, soit injecter du contexte. La configuration vit dans settings.json (global, projet, ou local).

Cycle d'un appel d'outil : PreToolUse avant l'outil, PostToolUse après, Stop à la fin du tour PreToolUsebloque / valide Outil PostToolUseformat / lint Stopfin du tour
Les points d'accroche autour d'un appel d'outil. (+ UserPromptSubmit, Notification, SubagentStop.)

Les événements les plus utiles :

ÉvénementSe déclenche…Usage typique
PreToolUseavant un appel d'outilbloquer une commande, valider
PostToolUseaprès un appel d'outilformater, linter, tester
Stopquand Claude veut s'arrêterforcer une vérif finale
UserPromptSubmità chaque message envoyéinjecter du contexte
Notificationquand Claude attendnotification bureau

PostToolUse : auto-format après chaque édition

Le hook qui change la vie. Dès que Claude écrit ou modifie un fichier, on le formate. Plus jamais de diff pollué par de l'indentation. Le matcher cible les outils Edit et Write :

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path'); [ \"${f##*.}\" = go ] && gofmt -w \"$f\"; true"
          }
        ]
      }
    ]
  }
}

Le hook reçoit le JSON de l'appel sur stdin ; jq en extrait le chemin du fichier. On formate uniquement si c'est du Go. Le ; true final garantit un code de sortie 0, sinon une erreur de format bloquerait Claude pour rien.

PreToolUse : bloquer ce qui ne doit pas passer

Le PreToolUse peut refuser un appel d'outil avant qu'il ne s'exécute. Deux façons : un code de sortie 2 (bloque et renvoie stderr au modèle), ou un JSON de décision plus fin. Exemple : interdire toute édition du .env, peu importe combien le modèle insiste.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path'); case \"$f\" in *.env|*secrets*) echo 'Fichier protégé' >&2; exit 2;; esac"
          }
        ]
      }
    ]
  }
}

Code de sortie 2 = blocage, et le message sur stderr est renvoyé à Claude pour qu'il comprenne pourquoi. C'est plus fiable qu'une consigne dans le prompt : un hook ne se « oublie » jamais entre deux tours de contexte. Même logique défensive que pour un middleware qui filtre avant le handler.

Stop : forcer la finition sans boucle infinie

Le Stop se déclenche quand Claude estime avoir fini. On peut le renvoyer au travail : par exemple s'il reste des fichiers non testés. Mais attention : si le hook bloque l'arrêt et que Claude re-déclenche un Stop, et que le hook re-bloque… boucle infinie. La protection est le champ stop_hook_active dans le JSON reçu :

#!/usr/bin/env bash
# stop-guard.sh, refuse l'arrêt si des tests échouent, UNE seule fois
input=$(cat)
# si on est déjà dans une relance de Stop, on laisse passer (anti-boucle)
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! go test ./... >/dev/null 2>&1; then
  echo "Des tests échouent, corrige avant de t'arrêter." >&2
  exit 2
fi
exit 0

Le garde stop_hook_active est le détail qui sépare un hook utile d'un agent coincé en boucle. Sans lui, le moindre test rouge transforme ta session en boucle sans fin. C'est exactement le réflexe d'un contexte d'annulation : prévoir la condition de sortie avant de lancer la boucle.

UserPromptSubmit : injecter du contexte automatiquement

Ce hook s'exécute à chaque message que tu envoies, avant que Claude le lise. Ce que le hook écrit sur stdout est injecté dans le contexte. Pratique pour rappeler une convention projet, l'heure, ou l'état du git :

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "echo \"Branche git : $(git branch --show-current 2>/dev/null)\"" }
        ]
      }
    ]
  }
}

À utiliser avec parcimonie : tout ce que tu injectes consomme des tokens à chaque message. Un rappel court et utile, pas un dump.

Les pièges à connaître

Un hook tourne avec tes permissions. C'est du shell exécuté sur ta machine, sans confirmation. Ne mets jamais un hook qui exécute une partie du JSON reçu sans la valider, c'est une porte d'injection. Traite la sortie du modèle comme une entrée non fiable.

Code de sortie ou JSON, pas les deux à la moitié. Exit 2 bloque, 0 laisse passer, le reste est non bloquant. Pour un contrôle fin (autoriser/refuser avec raison structurée), renvoie du JSON sur stdout plutôt que de jongler avec les codes.

La perf compte. Un hook PostToolUse lent s'exécute après chaque édition. Un gofmt, c'est instantané ; lancer toute la suite de tests à chaque écriture, non. Réserve le lourd au Stop.

Conclusion

Un hook n'est pas une consigne de plus dans ton prompt, c'est une garantie. La différence est énorme : une consigne, le modèle peut l'oublier au tour suivant quand le contexte se remplit ; un hook s'exécute toujours, à l'identique, parce que ce n'est plus le modèle qui décide. Tout ce que tu te surprends à répéter à Claude est un candidat pour un hook.

Commence par le PostToolUse de formatage, gain immédiat, zéro risque. Ajoute le Stop avec son garde anti-boucle quand tu veux verrouiller une vérif finale. Et garde en tête que tu câbles du shell sur une boucle pilotée par un modèle : la rigueur que tu mets dans tes services, mets-la dans tes hooks.

Commentaires (0)