Quand un hook d'agent IA plante, expire ou sort avec le mauvais code, l'agent le traite comme une erreur non bloquante et continue comme si le hook n'existait pas. Dans Claude Code, c'est le comportement par défaut documenté, et Codex comme Cursor font de même d'origine. Le garde-fou ne garde plus rien, le filtre de tokens ne filtre plus, et la session continue.
La seule trace est une notification d'une ligne dans le transcript. Cette page fait partie de la série sur les hooks de Claude Code : cet article-là détaille ce que coûte chaque événement de hook en tokens, celui-ci traite de ce qui se passe quand le hook lui-même casse.
Comportements lus dans la référence des hooks de Claude Code, la documentation des hooks de Codex et celle des hooks de Cursor, le 15/10/2026.
Que veut dire « fail open » pour un hook d'agent ?
Un hook échoue en mode ouvert (fail open) quand son échec laisse passer l'action. L'inverse, l'échec en mode fermé (fail closed), bloque l'action dès que le hook ne peut pas répondre.
La référence de Claude Code le dit sans détour : « On most events, when a hook fails or times out, Claude Code still carries out the action, so a policy hook with a wrong path or a crashing script lets everything through » (référence, block the action when a hook fails). Autrement dit : sur la plupart des événements, un hook en échec ou en timeout n'empêche rien, et un hook de contrôle avec un mauvais chemin laisse tout passer.
Ce comportement est raisonnable pour la plupart des hooks. Une notification de bureau cassée ne doit pas figer votre agent. Il fait mal sur exactement deux types de hooks :
- Les garde-fous : les hooks qui refusent
rm -rf, bloquent les écritures hors desrc/ou empêchent des secrets de quitter l'ordinateur où vous travaillez. - Les filtres dont dépend votre budget : les hooks qui réécrivent ou compactent la sortie des outils pour qu'une longue session reste abordable.
Quand un garde-fou échoue en mode ouvert, vous perdez la protection. Quand un filtre échoue en mode ouvert, vous ne voyez rien, et la facture monte en silence.
Comment se comporte chaque type d'échec dans Claude Code ?
Toute façon de casser un hook, hormis un exit 2 propre ou une décision JSON valide, finit en « erreur non bloquante » : l'action a lieu, et sur des événements comme PreToolUse une notification <hook name> hook error apparaît (référence, exit code output).
| Échec | Ce que fait Claude Code par défaut | Ce que vous voyez |
|---|---|---|
| Exit 1 (ou tout code autre que 0 et 2), sans JSON valide | Erreur non bloquante, l'action a lieu | Notification avec la première ligne de stderr |
| Exit 1 avec un JSON valide sur stdout | Succès : les champs JSON décident | Rien d'anormal |
| JSON illisible ou rejeté par la validation de schéma, exit 0 | Erreur non bloquante, l'action a lieu | Notification avec le message d'analyse ou de validation |
| Script introuvable ou non exécutable (le shell sort en 127) | Erreur non bloquante, l'action a lieu | Failed with non-blocking status code: /bin/sh: ...: No such file or directory |
Hook command, http ou mcp_tool qui atteint son timeout sur PreToolUse | Hook annulé, sortie ignorée, l'appel suit le circuit normal des permissions | timed out dans le log de débogage |
| Hook HTTP qui reçoit un statut non-2xx ou ne peut pas se connecter | Erreur non bloquante, l'exécution continue | Notification |
Exit 1 ne bloque pas
La référence consacre un encadré d'avertissement à ce piège : « Without valid JSON on stdout, Claude Code treats exit code 1 as a non-blocking error, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use exit 2. »
Toutes les habitudes Unix disent que 1 veut dire « non ». Dans un hook Claude Code, 1 veut dire « le hook a eu un problème, on continue ». C'est exit 2 qui bloque : sur PreToolUse, il arrête l'appel d'outil et votre stderr devient la raison.
Du texte sur stdout avant le JSON
Claude Code n'analyse stdout comme du JSON que « when the whole output is one JSON object with nothing around it but whitespace », c'est-à-dire un seul objet JSON entouré au plus d'espaces. Un profil shell qui affiche un message d'accueil, un set -x oublié ou un echo de débogage transforme votre décision en texte brut ou en erreur d'analyse.
Une erreur d'analyse est une erreur non bloquante : un permissionDecision: "deny" précédé d'une bannière ne refuse rien. Sur les événements qui ajoutent stdout au contexte, Claude Code écarte aussi la sortie qu'il n'a pas réussi à analyser.
Un binaire absent ou un chemin mal tapé
Quand le chemin du script n'existe pas, le shell sort en 127 et vous recevez une notification non bloquante. Le conseil de la référence elle-même : surveillez cette notification au premier lancement, car « a mistyped path in settings.json means the hook never runs ».
La version la plus sournoise, c'est une dépendance à l'intérieur du script. Les exemples Bash de la référence utilisent jq et demandent de vérifier qu'il est dans votre PATH. Si jq manque, un garde-fou typique extrait une commande vide, ne trouve aucune correspondance et sort en 0. Ce n'est même pas une erreur de hook : le script a réussi à ne rien faire.
Les hooks tournent « with Claude Code's environment », donc avec l'environnement de ce qui a lancé Claude Code. Un binaire placé dans un dossier que votre shell interactif ajoute au PATH peut manquer quand l'agent est lancé par un IDE, une tâche cron ou un runner de CI.
Les timeouts
Le timeout par défaut est de 600 secondes pour les hooks command, http et mcp_tool, abaissé à 30 sur UserPromptSubmit, PreModelSwitch et PostModelSwitch (référence, common fields). Un hook bloqué sur PreToolUse retient donc l'appel jusqu'à dix minutes, puis le laisse passer.
La référence est explicite : un hook PreToolUse de type command arrivé à son timeout « doesn't block the tool call... so don't count on a stalled hook to act as a gate ». PreModelSwitch fait exception : un hook annulé à son timeout bloque le changement de modèle. Les hooks callback de l'Agent SDK qui expirent sur PreToolUse bloquent aussi.
Codex et Cursor échouent-ils aussi en mode ouvert ?
Oui, les deux échouent en mode ouvert par défaut, et Cursor offre un réglage pour changer ça.
Codex. La documentation de Codex indique qu'un champ non pris en charge dans une réponse PreToolUse, comme continue: false ou permissionDecision: "ask", fait marquer le hook en échec, signaler l'erreur, « and continues the tool call » : l'appel d'outil a lieu.
Pour les hooks MCP distants gérés par l'administrateur, la même page prévient qu'« a PreToolUse callback error, timeout, or malformed response can fail the hook without blocking the tool ». Exit 2 avec une raison sur stderr bloque ; le timeout par défaut est de 600 secondes pour la plupart des hooks (hooks Codex).
La documentation ne prévoit aucune option de blocage en cas d'échec pour les hooks command locaux. Une demande ouverte, openai/codex#41979 (ouverte le 01/09/2026), décrit le comportement actuel ainsi : un timeout, un échec de lancement, un plantage ou une sortie mal formée « is treated as a failed hook with should_block=false, so the tool is dispatched », et réclame un mode bloquant optionnel.
Codex ajoute sa propre façon de désactiver un hook sans bruit : les hooks non gérés doivent être revus et approuvés, et un hook nouveau ou modifié est « skipped until trusted ». Codex affiche alors un avertissement au démarrage qui renvoie vers /hooks : lisez-le après chaque modification de hooks.json.
Cursor. Pour beforeShellExecution et beforeMCPExecution, « crashes, timeouts, and non-zero exit codes other than 2 fail open by default: Cursor logs the failure and allows the action through ».
Ajouter failClosed: true sur un hook fait que les échecs (plantage, timeout, code de sortie non nul, absence de sortie) bloquent l'action au lieu de la laisser passer. Les hooks de permission de Cursor bloquent déjà sur un JSON invalide, même sans ce réglage (hooks Cursor).
Comment savoir qu'un hook a échoué ?
On le sait en le cherchant exprès, parce que l'agent ne le dira pas au modèle, et souvent pas à vous non plus. Quatre vérifications, de la moins coûteuse à la plus coûteuse.
- Lisez la notification au premier lancement. Dans Claude Code, une erreur non bloquante laisse une ligne
<hook name> hook errordans le transcript. Elle est courte et facile à rater, mais elle nomme l'échec. - Ouvrez
/hooks. Le menu liste chaque hook configuré avec sa source (paramètres utilisateur, projet, locaux, plugin ou session) et la commande exacte qu'il lance. Un hook absent de cette liste ne tourne jamais, quoi qu'en dise votre fichier de paramètres. - Activez le log de débogage. Lancez
claude --debug-file <path>, ouclaude --debugpuis lisez~/.claude/debug/<session-id>.txt(référence, debug hooks). Depuis la v2.1.296, chaque hook command sur un événement d'outil laisse une ligneHooks:qui finit parfinished with status 0 (31ms),timed out after <N>msoufailed to start. Cherchez les deux dernières. - Lancez un canari. Faites refuser à votre garde-fou une sentinelle inoffensive, par exemple toute commande Bash contenant
HOOK_CANARY. Au début d'une longue session de nuit, demandez à l'agent de lancerecho HOOK_CANARY. Si le mot s'affiche, le garde-fou est tombé.
Pour un filtre, le canari, c'est la taille de la sortie. Lancez une commande bavarde que vous connaissez, comme votre suite de tests, et regardez ce que le modèle a reçu. S'il a lu 4 000 lignes au lieu de 40, le filtre ne se charge pas. Mesurer l'usage de tokens d'un agent montre où trouver ces chiffres dans le transcript.
Comment faire échouer un hook sans risque ?
Décidez, hook par hook, si un échec doit bloquer ou laisser passer, puis écrivez-le dans la configuration. Claude Code a désormais un champ pour ça.
Mettez onFailure: "block" sur les garde-fous. Sur un hook command ou http, "onFailure": "block" transforme tout échec en ce que fait exit 2 sur cet événement. Il faut Claude Code v2.1.295 ou plus récent. La référence compte comme échecs : un hook qui ne démarre pas, un code de sortie autre que 0 ou 2 (même avec un JSON qui autorise), une erreur HTTP, un timeout et un JSON invalide.
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
"onFailure": "block"
}
]
}
]
}
}
Le test proposé par la référence est le bon : supprimez le script, demandez un ls, et attendez failed; blocking because onFailure is "block". Notez les limites : le champ n'a aucun effet sur Stop, SubagentStop, TaskCompleted et TeammateIdle, ni sur les hooks async.
Bloquez avec exit 2 ou en JSON, jamais avec exit 1. Exit 0 avec un permissionDecision en JSON, ou exit 2 avec une raison sur stderr. Tout le reste est une erreur non bloquante.
Gardez stdout propre. N'y écrivez que l'objet JSON. Envoyez les lignes de débogage sur stderr, que Claude ne voit jamais quand le hook sort en 0.
Utilisez la forme exec pour les scripts. Avec args, Claude Code résout command dans le PATH et le lance sans shell : les guillemets autour de ${CLAUDE_PROJECT_DIR} ne peuvent plus casser le chemin. Utilisez des chemins absolus, ou vérifiez que chaque binaire appelé par votre script existe dans l'environnement de l'agent.
Faites échouer le script bruyamment. En Bash, set -euo pipefail transforme un jq manquant en code de sortie non nul au lieu d'une variable vide. Combiné à onFailure: "block", cette dépendance manquante bloque au lieu d'approuver.
Choisissez un timeout supportable. Dix minutes, c'est long pour un garde-fou. Si votre vérification prend d'habitude 200 ms, un timeout de 5 secondes avec onFailure: "block" fait qu'une vérification bloquée vous coûte un appel refusé, pas dix minutes suivies d'un appel non contrôlé.
Sur Cursor, utilisez failClosed: true. Sur Codex, faute de réglage équivalent, le canari est votre filet de sécurité.
Un filtre de tokens doit-il échouer en mode ouvert ou fermé ?
Un filtre doit échouer en mode ouvert, et vous devez le surveiller comme un garde-fou. Les deux ne se contredisent pas.
Si un hook de compaction casse en mode fermé, chaque appel Bash est bloqué et l'agent ne peut plus travailler. C'est pire que payer un log bavard. Le mode ouvert laisse le travail continuer, avec un coût qui revient à ce qu'il était avant le filtre.
Ce coût est réel sur les longues sessions. Sur le benchmark THOL, Tokenade réduit le coût des sessions de 39 % sur les longues sessions (méthodologie). Un filtre qui cesse de se charger sans prévenir rend tout cela, sans une seule erreur à l'écran.
La règle que j'applique :
- Garde-fous :
onFailure: "block"oufailClosed: true, un timeout court, un canari au début de chaque session. - Filtres : mode ouvert, plus une vérification qu'ils tournent vraiment. Pour Tokenade,
tokenade healthindique si ses hooks sont installés et donne la correction quand ils ne le sont pas. - Tout le reste (notifications, logs, formatage) : gardez le comportement par défaut.
Pour la technique de filtrage elle-même, voir le filtrage de sortie des logs de commandes.
Ce qui tourne mal (anti-patterns)
- Ne tester que le cas nominal. Vous avez vérifié que
lspasse toujours. Vous n'avez jamais vérifié querm -rf /tmp/xest bloqué. Un garde-fou que vous n'avez jamais vu se déclencher n'est pas un garde-fou. - Prendre une session sans erreur pour une preuve. L'absence de notification peut vouloir dire que le hook marche, ou qu'il n'est pas enregistré du tout.
/hooksvous dit lequel des deux. - Modifier le script, pas la configuration. Renommez
check.shencheck-command.shet l'ancien chemin danssettings.jsonsort désormais en 127 à chaque appel. - Supposer qu'un autre agent copie Claude Code. Cursor bloque sur un JSON invalide dans ses hooks de permission ; Claude Code et Codex non. Porter un garde-fou, c'est aussi porter sa façon d'échouer, ou pas.
FAQ
Pourquoi mon hook Claude Code ne bloque-t-il rien ?
Les causes habituelles : exit 1 au lieu de 2, du texte affiché avant le JSON, un chemin de script qui n'existe pas, une dépendance manquante comme jq, ou un timeout sur PreToolUse. Ce sont toutes des erreurs non bloquantes par défaut, donc l'action a lieu. Ouvrez /hooks pour vérifier qu'il est enregistré, puis cherchez failed to start ou timed out dans le log de débogage.
Quel est le timeout par défaut d'un hook Claude Code ?
600 secondes pour les hooks command, http et mcp_tool, 30 pour les hooks prompt et 60 pour les hooks agent. Les trois premiers descendent à 30 secondes sur UserPromptSubmit, PreModelSwitch et PostModelSwitch. Un hook command PreToolUse arrivé à son timeout laisse passer l'appel d'outil, sauf si vous avez mis onFailure: "block".
Comment faire échouer un hook Claude Code en mode fermé ?
Ajoutez "onFailure": "block" à un hook command ou http (Claude Code v2.1.295 ou plus récent). Un échec fait alors ce que fait exit 2 sur cet événement : sur PreToolUse, il bloque l'appel d'outil. Testez en supprimant le script puis en lançant une commande que le hook intercepte ; l'erreur doit contenir blocking because onFailure is "block".
Les hooks Codex échouent-ils en mode ouvert ?
Oui. La documentation de Codex indique qu'une réponse PreToolUse avec un champ non pris en charge est marquée en échec et que l'appel d'outil continue, et que les erreurs, timeouts ou réponses mal formées des hooks MCP gérés peuvent échouer sans bloquer. Aucune option de blocage n'est documentée pour les hooks command locaux ; l'issue GitHub #41979 en demande une.
Un hook de réduction de tokens doit-il bloquer quand il échoue ?
Non. Un filtre qui échoue en mode fermé empêche l'agent de lancer la moindre commande. Laissez-le échouer en mode ouvert, puis vérifiez qu'il est chargé : lancez une commande bavarde et regardez combien de sortie le modèle a reçue, ou utilisez la vérification d'état de l'outil.
À lire aussi
- Hooks de Claude Code : lesquels ajoutent des tokens, lesquels en retirent : la carte des coûts de chaque événement de hook.
- Filtrage de sortie des logs de commandes : ce que fait concrètement un hook de réduction de tokens.
- Sécuriser le budget d'un agent la nuit : là où un filtre ou un garde-fou mort fait le plus mal.
- Mesurer l'usage de tokens d'un agent : vérifier un filtre à partir du transcript.
- Réduire l'usage de tokens de Claude Code : la liste complète des leviers.
- Les meilleurs optimiseurs de tokens pour Claude Code : des outils qui livrent ces hooks tout faits.
Réduisez la facture de tokens de votre agent IA.
Classé n°1 au Token-Harness Optimizer Leaderboard. Zéro config.
Tokenade réduit la facture de tokens des agents IA : une installation, zéro config, et il allège ce que votre agent envoie au modèle.