Hooks Claude Code : exemples qui bloquent, avec tests
Les hooks Claude Code sont des commandes qui s’exécutent à des moments précis d’une session : avant l’exécution d’un outil, après son exécution, à la soumission d’un prompt, à l’arrêt de la session. C’est le moyen de transformer « l’agent ne doit pas faire ça » en quelque chose de réellement vérifié. Cet article présente des exemples de hooks Claude Code qui bloquent ce qui doit être bloqué, comment les tester, et les erreurs derrière la plupart des recherches « hook error ».
Tout ce qui suit s’appuie sur la référence officielle des hooks et la documentation des permissions. Nous utilisons Claude Code avec des hooks tous les jours pour notre studio ; l’un des nôtres est un hook Stop qui avertit quand la consommation de tokens d’une session dépasse une limite.
Où sont configurés les hooks Claude Code
Les hooks vivent dans les fichiers de paramètres :
| Fichier | S’applique à | Partagé |
|---|---|---|
~/.claude/settings.json |
tous vos projets | non |
.claude/settings.json |
un projet | oui, peut être versionné |
.claude/settings.local.json |
un projet | non |
Les entrées de ces fichiers sont fusionnées, pas remplacées. Les plugins, les skills et les sous-agents peuvent aussi apporter leurs propres hooks.
Une entrée de hook a trois niveaux : l’événement, un groupe matcher, et les handlers :
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/block_rm.py" }
]
}
]
}
}
CLAUDE_PROJECT_DIR est la racine du projet où la session a démarré. Sous Windows, appelez Python comme votre système l’attend.
Un hook PreToolUse qui bloque une commande risquée
Un hook PreToolUse reçoit l’appel d’outil prévu en JSON sur l’entrée standard : tool_name, tool_input et quelques champs de session. Pour Bash, tool_input.command est le texte de la commande.
#!/usr/bin/env python3
import json, re, sys
data = json.load(sys.stdin)
cmd = data.get("tool_input", {}).get("command", "")
if re.search(r"\brm\s+-\w*(rf|fr)\w*\b", cmd):
print("Blocked: recursive force delete. Ask the user to run it.", file=sys.stderr)
sys.exit(2)
sys.exit(0)
La règle qui compte : le code de sortie 2 bloque l’appel, et stderr devient la raison que Claude voit. Le code 0 laisse passer. Tout autre code est une erreur non bloquante : l’action a lieu et vous recevez seulement une notification.
Un motif textuel capture la forme habituelle d’une commande, pas toutes ses formes : celui-ci arrête rm -rf et rm -fr, mais rm -r -f avec des drapeaux séparés passe. C’est pourquoi chaque règle a besoin de tests, et pourquoi les règles de permission et le sandbox existent à côté des hooks.
Si vous préférez une sortie structurée, un hook peut se terminer avec le code 0 et afficher un JSON avec hookSpecificOutput, où hookEventName vaut PreToolUse et permissionDecision vaut allow, deny, ask ou defer, avec un permissionDecisionReason.
Le matcher PreToolUse, sans surprises
Le matcher filtre sur le nom de l’outil :
Bash, ou une liste commeEdit|Write— noms exacts.- Toute valeur avec d’autres caractères est une expression régulière, non ancrée :
^Notebook,mcp__memory__.*. - Les outils MCP sont nommés
mcp__<server>__<tool>. Un matcher valantmcp__memoryseul ne correspond à rien ; il fautmcp__memory__.*. *, une chaîne vide, ou l’absence de matcher signifie tous les outils.
Il existe aussi un champ if qui utilise la syntaxe des règles de permission, comme Bash(git *). La documentation le qualifie de best-effort : pour un allow ou un deny ferme, utilisez le système de permissions.
Testez chaque hook : un appel bloqué, un appel autorisé
Un hook jamais testé est un hook dont vous espérez qu’il fonctionne. Le test le moins coûteux lui envoie le même JSON que Claude Code enverrait et vérifie le code de sortie :
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf build"}}' | python3 .claude/hooks/block_rm.py; echo "exit $?" # expect 2
echo '{"tool_name":"Bash","tool_input":{"command":"ls build"}}' | python3 .claude/hooks/block_rm.py; echo "exit $?" # expect 0
Notre règle pour chaque hook : une action qui doit être bloquée et une qui doit passer, toutes deux dans le fichier de test. Le second test attrape l’autre panne fréquente : un motif si large qu’il bloque le travail normal.
Pourquoi une erreur de hook PreToolUse n’arrête rien
Les causes habituelles, toutes issues de la référence :
- Code de sortie 1 au lieu de 2. Une notification d’erreur s’affiche et l’outil s’exécute quand même.
- Timeout. Un hook de commande PreToolUse qui expire ne bloque pas ; l’appel continue par le flux normal de permissions. Les hooks de commande ont par défaut 600 secondes.
- Un matcher qui ne correspond jamais, comme
mcp__memorysans.*. - Un script en échec. Par défaut, un hook en échec ne bloque pas. Les versions récentes prennent en charge
"onFailure": "block"(v2.1.295 ou ultérieure) pour bloquer quand un hook de commande échoue, expire ou renvoie une sortie invalide.
Hooks, règles de permission et skills : qui fait quoi
- Les règles de permission (
allow,ask,denydans settings.json) sont vérifiées dans cet ordre : deny, puis ask, puis allow. Une règle deny s’applique même quand un hook PreToolUse renvoieallow. - Un hook bloquant (code 2) arrête l’appel avant l’évaluation des règles de permission, il l’emporte donc aussi sur une règle allow.
- Les skills sont des instructions et des ressources que l’agent charge au besoin. Ils guident ; ils n’imposent rien.
Pour les dossiers confidentiels, commencez par des règles deny comme Read(./secrets/**). Connaissez leur limite : elles couvrent les outils de fichiers intégrés et des commandes comme cat, head et tail, mais pas un grep -r lancé depuis le dossier, ni un script qui ouvre lui-même des fichiers. Un hook PreToolUse peut inspecter le texte complet de la commande dans ces cas-là, et pour un contrôle au niveau du système d’exploitation, la documentation renvoie au sandbox.
Quand confier cela à quelqu’un
Si votre installation d’agents a grandi vite et que vous ne savez plus quelles règles tiennent vraiment, nous cartographions les agents, écrivons les règles de hook avec leurs tests et montrons l’exécution des tests, pas seulement la configuration. Vous pouvez voir comment notre propre système est construit sur la page réalisations.
Questions
Pourquoi mon hook PreToolUse ne bloque-t-il rien ?
Le plus souvent, il se termine avec le code 1. Seul le code de sortie 2 bloque l'appel d'outil ; tout autre code non nul est une erreur non bloquante et l'action est exécutée. Un hook qui expire ne bloque pas non plus.
Ai-je besoin de hooks si j'ai déjà des règles deny de permissions ?
Pour des règles simples, les règles deny passent en premier : elles sont vérifiées même quand un hook renvoie allow. Les hooks servent pour les logiques qu'un motif ne peut pas exprimer, comme inspecter le texte complet d'une commande avant son exécution.
Les règles deny sur Read empêchent-elles cat et grep dans Bash ?
Elles couvrent les outils de fichiers intégrés et les commandes de fichiers que Claude Code reconnaît dans Bash, comme cat, head et tail. Elles ne couvrent pas une commande qui lit des fichiers sans les nommer, comme un grep -r lancé depuis le dossier, ou un script qui ouvre lui-même des fichiers. Pour cela, la documentation renvoie au sandbox.
Où sont stockés les hooks Claude Code ?
Dans les fichiers de paramètres : ~/.claude/settings.json pour tous vos projets, .claude/settings.json pour un projet et votre équipe, ou .claude/settings.local.json pour vous seul. Les entrées de plusieurs fichiers sont fusionnées.