Pièges courants des hooks
Sur cette page
Votre hook fonctionne sur votre machine. Vous le commitez, un collègue récupère la branche, et rien ne se déclenche chez lui. Ou bien votre hook de sécurité a un chemin faux et laisse tout passer sans un mot. Ou encore il bloque Claude en boucle, huit fois de suite. Les hooks sont simples, mais les façons de se tromper sont connues et documentées. Cette leçon les passe en revue, du partage en équipe au débogage.
1. Les chemins et le partage en équipe
Le piège historique : un chemin absolu vers le script (/Users/vous/dev/projet/hooks/check.js) qui n’existe nulle part chez un collègue, alors qu’un chemin relatif dépend du répertoire courant au moment de l’appel. La solution est le placeholder ${CLAUDE_PROJECT_DIR}, remplacé par la racine du projet où la session a démarré, combiné à la forme exec (command plus args), qui lance l’exécutable sans shell, chaque élément de args étant passé tel quel :
{ "type": "command", "command": "node", "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check.js"] }
En forme shell (sans args), entourez le placeholder de guillemets : "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check.sh". Sur macOS et Linux, un script appelé directement doit être exécutable (chmod +x). Dans un worktree, ${CLAUDE_PROJECT_DIR} reste la racine d’origine ; c’est le champ cwd du JSON reçu qui suit Claude. Pour un plugin, ${CLAUDE_PLUGIN_ROOT} joue le même rôle.
2. La confiance accordée au dossier
En session interactive, Claude Code retient tous les hooks des fichiers de réglages, y compris votre ~/.claude/settings.json, tant que vous n’avez pas accepté le dialogue de confiance du dossier. Un hook qui « ne fait rien » dans un dépôt fraîchement cloné est souvent un dépôt pas encore approuvé.
L’inverse est plus dangereux : en mode non interactif (claude -p, Agent SDK), le dialogue n’existe pas et le dossier est traité comme de confiance. Les hooks commités dans le .claude/settings.json d’un dépôt que vous n’avez jamais relu s’exécutent donc sur votre machine. Avant de scripter sur un dépôt tiers, lisez son dossier .claude/, démarrez avec --bare, ou désactivez les hooks pour ce run : --settings '{"disableAllHooks": true}'.
3. L’échec qui laisse passer
Un hook qui ne démarre pas (script introuvable), sort avec un code autre que 0 ou 2, dépasse son délai ou imprime du JSON invalide est une erreur non bloquante : l’action continue, avec un avis hook error dans la conversation. Pour une automatisation de confort, c’est le bon comportement. Pour un garde-fou, c’est une faille : un hook de sécurité mal installé ne protège rien. Ajoutez "onFailure": "block" aux hooks command et http qui doivent fermer la porte en cas de doute (version 2.1.295 et au-delà).
Les délais par défaut sont de 10 minutes pour command, http et mcp_tool, abaissés à 30 secondes sur UserPromptSubmit (un hook lent y bloque chaque prompt), 30 secondes pour prompt, 60 pour agent, et 1,5 seconde partagée pour tous les hooks SessionEnd. Le champ timeout les ajuste. Un hook command déclaré async: true tourne en arrière-plan sans délai, mais ne peut rien bloquer.
4. Codes de sortie contre JSON
Choisissez une approche par hook. Si vous mélangez, le code 2 garde son effet bloquant et le JSON ne peut pas le lever, même avec permissionDecision: "allow". Et le JSON doit être la seule chose sur stdout : une ligne affichée par votre .zshrc ou .bashrc au démarrage, un echo de débogage, et Claude Code lit du texte brut au lieu d’un objet. Si un hook JSON « n’a aucun effet », c’est la première chose à vérifier ; les parses ratés sont dans le journal de débogage.
5. Le matcher ne voit que ce qu’il nomme
Les matchers sont sensibles à la casse (bash ne correspond pas à Bash) et deviennent des expressions régulières non ancrées dès qu’ils contiennent un caractère spécial : Edit.* correspond aussi à NotebookEdit. Plus important, Claude peut créer ou modifier un fichier par une commande shell. Si votre hook doit voir chaque changement (audit, conformité), ajoutez Bash|PowerShell au matcher et faites lister les fichiers modifiés par git status --porcelain, ou utilisez un hook Stop qui inspecte l’arbre de travail une fois par tour, ou FileChanged, qui surveille un fichier sur disque quel que soit ce qui l’écrit. Sous Windows, Bash seul ne couvre pas l’outil PowerShell.
Sous Windows aussi, les chemins arrivent avec des barres obliques inverses : une comparaison écrite avec /src/ ne correspond jamais à C:\projet\src\index.ts. Normalisez (FILE_PATH="${FILE_PATH//\\//}" en Bash, .replace("\\", "/") en Python ou Node) avant de comparer.
6. Ce qu’un hook ne peut pas défaire, et les boucles
PostToolUse ne peut pas annuler : l’outil a tourné. Il peut remplacer ce que Claude voit du résultat (updatedToolOutput), pas ce qui s’est passé sur le disque ou le réseau ; pour empêcher, c’est PreToolUse. Et Stop se déclenche à chaque fin de réponse, pas seulement quand la tâche est finie : un hook Stop qui renvoie « continue, ce n’est pas terminé » sans condition d’arrêt tourne en rond. Lisez le champ stop_hook_active de l’entrée, qui vaut true quand Claude continue déjà à cause d’un hook Stop ; Claude Code coupe de toute façon après huit continuations consécutives.
Quand plusieurs hooks correspondent au même événement, ils tournent en parallèle et le verdict le plus restrictif l’emporte ; un deny n’empêche pas un hook voisin d’avoir déjà écrit dans un journal. Deux hooks PreToolUse qui renvoient chacun updatedInput pour le même outil donnent un résultat indéfini : un seul hook réécrit un outil donné.
7. Vérifier et déboguer
Un hook modifié dans un fichier de réglages est relu par le surveillant de fichiers ; en cas de doute, /hooks montre ce que la session a chargé, par événement. Pour voir ce qu’un hook a reçu et renvoyé, lancez claude --debug et lisez ~/.claude/debug/<session-id>.txt (ou /debug en cours de session) : chaque exécution y laisse une ligne Hooks: PostToolUse:Write [...] finished with status 0 (31ms), et les sorties JSON illisibles y sont expliquées. Un hook lent se repère à la durée en fin de ligne. Pour tout désactiver sans rien supprimer : "disableAllHooks": true.
À vous de jouer
- Dans un hook
PreToolUsesurBash, mettez un chemin de script faux. Demandezls: l’action passe et unhook errors’affiche. Ajoutez"onFailure": "block": elle est bloquée avec le message d’erreur de Node. - Ajoutez un
echo "coucou"avant la sortie JSON d’un hook et constatez, avecclaude --debug, que le JSON n’est plus lu. - Clonez votre dépôt dans un autre dossier et ouvrez
claudededans : tant que vous n’avez pas répondu au dialogue de confiance,/hooksne liste pas les hooks du projet. - Écrivez un hook
Stopde typepromptqui demande si la tâche est finie, et lisezstop_hook_activedans le journal après deux tours.
Sources officielles : Hooks reference · Automate actions with hooks · Configure permissions · Run Claude Code programmatically
Points clés à retenir
${CLAUDE_PROJECT_DIR}et la forme exec remplacent les chemins absolus : un hook commité fonctionne chez tout le monde- Les hooks d’un dossier ne tournent qu’après le dialogue de confiance ; en mode
-p, ils tournent sans dialogue, relisez.claude/d’un dépôt tiers - Un hook qui échoue laisse passer l’action :
onFailure: "block"pour les garde-fous ; surveillez les délais - JSON seul sur stdout, codes de sortie ou JSON mais pas les deux ; matchers sensibles à la casse ;
Bash|PowerShellouFileChangedpour voir les écritures par commande PostToolUsene défait rien ;Stopse déclenche à chaque fin de réponse, testezstop_hook_active/hookspour vérifier,claude --debugpour comprendre