Hooks Claude Code : fonctionnement, événements et sécurité
Découvrez où configurer les hooks Claude Code, les événements déclenchés, le rôle du code de sortie 2 et les risques de sécurité liés à leur exécution automatique.
Qu’est-ce qu’un hook Claude Code
Les hooks Claude Code sont des commandes shell que Claude Code exécute automatiquement à des points précis de son propre cycle de vie. C’est toute la différence entre un hook et un fichier de règles. Une instruction dans CLAUDE.md est un conseil, et le modèle l’évalue avec le reste de son contexte. Un hook est du code, et il s’exécute que le modèle soit d’accord ou non. Si votre agent ignore systématiquement le formatter que vous lui avez indiqué à deux reprises, vous n’avez pas besoin d’une instruction plus ferme. Vous avez besoin d’un hook.
Le mécanisme est simple. Vous enregistrez une commande dans un fichier de configuration, sous le nom d’un événement. Lorsque cet événement se produit, Claude Code exécute votre commande et écrit les données de l’événement sur son entrée standard (stdin) au format JSON (JavaScript object notation). Votre commande lit ces données, effectue son traitement, puis renvoie un code de sortie. Le code de sortie 2 d’un hook PreToolUse annule l’appel de l’outil avant son exécution, et tout ce que votre script écrit sur la sortie d’erreur standard (stderr) est transmis au modèle comme motif.
Les noms d’événements et de champs présentés ici proviennent de la référence des hooks Claude Code, vérifiée en août 2026 avec la release 2.1.232. Cette interface évolue rapidement. Consultez donc la référence correspondant à votre version avant de copier du JSON depuis un article de blog, y compris celui-ci. Affichez la vôtre avec claude --version.
Emplacement de la configuration des hooks
Un hook est un bloc JSON dans un fichier de configuration. Six emplacements peuvent en contenir un. La portée du fichier détermine la portée du hook.
~/.claude/settings.json: tous les projets de votre machine, mais pas ceux des autres utilisateurs..claude/settings.json: un seul projet, versionné dans le dépôt. Toute personne qui le clone obtient donc le hook..claude/settings.local.json: un seul projet, uniquement sur votre machine.- Paramètres de stratégie gérés : à l’échelle de l’organisation, définis par un administrateur.
hooks/hooks.jsondans un plugin : actifs tant que ce plugin est activé.- Frontmatter d’une skill ou d’un subagent : actif tant que ce composant est actif.
Les entrées de hook provenant de ces fichiers sont fusionnées au lieu de se remplacer. Le fichier de configuration d’un projet ajoute ses hooks à ceux des paramètres utilisateur, au lieu de les remplacer. Un même événement peut donc contenir plusieurs hooks provenant de plusieurs fichiers. La définition de "disableAllHooks": true les désactive, sauf dans un cas : les hooks provenant des paramètres de stratégie gérés continuent de s’exécuter, sauf si cette définition est également appliquée dans les paramètres gérés.
Exécutez /hooks dans une session pour afficher tous les hooks actuellement enregistrés, regroupés par événement, avec le fichier source et le matcher de chacun. Ce menu est en lecture seule. Pour modifier un hook, éditez le fichier de configuration. Le file watcher détecte généralement la modification sans nécessiter de redémarrage.
Événements de hook disponibles dans Claude Code
La version 2.1.232 répertorie trente et un événements, de SessionStart à SessionEnd, qui couvrent la compaction, les sous-agents, les worktrees et les fichiers de configuration. Pour l’administration de serveurs, quelques-uns suffisent.
PreToolUse: avant l’exécution d’un appel d’outil. C’est le seul événement qui peut le bloquer.PostToolUse: après la réussite d’un appel d’outil.PostToolUseFailurese déclenche en cas d’échec. Un hook qui doit voir chaque résultat doit donc utiliser les deux.PermissionRequest: lorsqu’un appel d’outil nécessite une décision d’autorisation, c’est-à-dire au moment où la demande d’approbation devrait s’afficher.UserPromptSubmit: lorsque vous envoyez un prompt, avant que Claude ne le traite. Tout ce que ce hook écrit sur stdout est ajouté au contexte du modèle.SessionStartetSessionEnd: à chaque fin de session.SessionStartse déclenche également après une compaction, avec la valeur de matchercompact.Stop: lorsque Claude termine sa réponse. Cet événement se produit une fois par tour, et non une fois par tâche terminée.
Chaque groupe contient un matcher qui détermine les occurrences déclenchant le hook. Pour les événements liés aux outils, il filtre selon le nom de l’outil. Ainsi, "Edit|Write" se déclenche lors des modifications de fichiers, et dans aucun autre cas. Les matchers sont sensibles à la casse. Un matcher vide se déclenche à chaque occurrence. Les outils d’un serveur MCP (model context protocol) sont nommés mcp__<server>__<tool>. Un matcher "mcp__github__.*" intercepte donc les outils d’un serveur donné sans intercepter ceux des autres serveurs.
Les hooks Stop comportent un piège à connaître avant d’en écrire un. Un hook Stop qui bloque renvoie le modèle au traitement, et Claude Code ignore le hook après huit blocages consécutifs. Lisez le champ stop_hook_active dans l’entrée du hook et quittez avec le code 0 lorsqu’il vaut true. Sinon, votre hook bouclera jusqu’à atteindre cette limite.
Ce qu’un hook reçoit sur stdin
Lorsque Claude s’apprête à exécuter npm test, un hook PreToolUse sur Bash lit les données suivantes sur stdin :
{
"session_id": "abc123",
"cwd": "/home/deploy/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}Chaque événement contient session_id, cwd, permission_mode, transcript_path et hook_event_name. Les événements liés aux outils ajoutent tool_name, tool_input et tool_use_id. Les autres événements contiennent leurs propres champs : UserPromptSubmit reçoit le texte prompt, tandis que SessionStart reçoit un source de startup, resume, clear, compact ou fork.
jq est la méthode habituelle pour lire ces données dans un script shell. Une image de serveur minimale ne l’inclut pas. Installez-la d’abord avec sudo apt install -y jq sur Ubuntu et Debian.
Effet du code de sortie sur l’appel d’outil en cours
Il existe trois possibilités.
- Le code de sortie 0 signifie que votre hook ne s’y oppose pas. Pour
PreToolUse, cela ne vaut pas approbation et le flux normal de demande d’autorisation continue. PourUserPromptSubmitetSessionStart, stdout est ajouté au contexte du modèle. - Le code de sortie 2 bloque l’action pour les événements qui peuvent être bloqués, notamment
PreToolUse, et stderr devient le motif affiché au modèle. Pour les événements qui ne peuvent pas être bloqués, commePostToolUse, le blocage est ignoré, mais stderr est tout de même transmis au modèle comme retour d’information. - Tout autre code de sortie indique une erreur non bloquante. L’action continue. La transcription affiche un message d’erreur du hook contenant la première ligne de stderr après le texte
Failed with non-blocking status code:.
Pour faire autre chose que bloquer ou rester silencieux, utilisez le code de sortie 0 et affichez plutôt un objet JSON sur stdout. Un hook PreToolUse prend sa décision avec permissionDecision :
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" ignore l’invite interactive, "deny" annule l’appel et envoie le motif au modèle, et "ask" affiche l’invite normalement. Choisissez un seul mode par hook. Combiner le code de sortie 2 avec une décision JSON sur stdout produit un résultat dont le comportement doit être vérifié.
Lorsque plusieurs hooks correspondent au même événement, ils s’exécutent en parallèle et vont tous jusqu’à leur terme. Un deny d’un hook n’arrête pas ses hooks frères : un hook de journalisation peut donc continuer à écrire sa ligne pendant qu’un hook de garde refuse le même appel. Claude Code fusionne ensuite les réponses et conserve la plus restrictive, dans l’ordre suivant : deny, defer, ask, allow.
Exemple 1 : bloquer une commande destructive avant son exécution
Enregistrez ce fichier sous .claude/hooks/block-destructive.sh dans votre projet :
#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
exit 2
fi
done
exit 0Rendez-le exécutable, puis enregistrez-le dans PreToolUse, dans .claude/settings.json :
chmod +x .claude/hooks/block-destructive.sh{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10,
"statusMessage": "Checking the command against policy"
}
]
}
]
}
}Testez le script manuellement avant de lui faire confiance, car un hook qui plante sur sa propre entrée échoue en mode permissif :
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
| .claude/hooks/block-destructive.sh
echo $?Vous devez voir la ligne Blocked by policy: sur stderr et obtenir le code de sortie 2. Fournissez-lui une commande inoffensive, comme ls -la. Vous ne devez obtenir aucune sortie et le code de sortie doit être 0. Dans une session, l’appel refusé apparaît dans la transcription avec votre message comme motif. Le modèle lit ce message et s’adapte.
Une propriété rend cette méthode utile : les hooks PreToolUse sont exécutés avant la vérification du mode d’autorisation, quel que soit le mode d’autorisation. Un refus reste donc effectif, même avec bypassPermissions. C’est ce qui rend un hook utile avec le mode automatique de Claude Code et ses paramètres d’autorisation, lorsque les demandes de confirmation sont réduites, mais que le hook continue de s’exécuter.
Il faut être clair sur la portée de cette protection. La recherche de motifs dans une chaîne de commande protège contre la négligence d’un agent. Elle ne constitue pas une limite contre un agent capable de contourner la règle, car la même commande peut être écrite sous une forme que votre grep ne détecte pas. Les règles strictes doivent être définies dans le système d’autorisation et dans le compte utilisé par le processus.
Exemple 2 : formater et analyser après chaque modification
PostToolUse avec un filtre Edit|Write s’exécute après tout outil de modification de fichiers. Enregistrez ceci dans .claude/hooks/after-edit.sh :
#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
case "$FILE" in
*.py)
ruff format "$FILE" >/dev/null 2>&1
if ! ruff check "$FILE" >&2; then
exit 2
fi
;;
*.sh)
if ! shellcheck "$FILE" >&2; then
exit 2
fi
;;
esac
exit 0{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
"timeout": 60
}
]
}
]
}
}Demandez à Claude d’ajouter une fonction mal indentée dans un fichier Python, puis ouvrez le fichier. Il est correctement formaté. Cela confirme que le hook s’est exécuté, car un hook exécuté avec succès n’affiche rien dans la conversation.
Le code de sortie 2 n’annule rien ici. PostToolUse se déclenche après l’exécution de l’outil. La modification est donc écrite sur le disque dans tous les cas. Le code de sortie 2 permet de transmettre la sortie de ruff check au modèle comme retour d’information. Claude corrige ainsi l’erreur qu’il vient d’introduire au lieu de poursuivre. C’est la différence entre une erreur d’analyse détectée au moment du commit et une erreur que l’agent corrige au cours de la même interaction.
Deux limites des filtres sont importantes ici. Edit|Write ne détecte pas les fichiers modifiés par une commande shell, et Claude écrit assez souvent les fichiers via Bash pour que cette lacune soit réelle. Pour couvrir chaque appel, faites également correspondre Bash et demandez au script de lister les fichiers modifiés avec git status --porcelain. Pour couvrir chaque interaction une seule fois, placez plutôt l’analyse dans un hook Stop.
Exemple 3 : journaliser chaque appel d’outil à des fins d’audit
Un matcher vide dans PostToolUse s’exécute pour chaque outil. L’envoi de l’enregistrement vers le journal système plutôt que vers un fichier du répertoire personnel le met hors de portée du shell de l’agent :
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
}
]
}
]
}
}Relisez les entrées avec journalctl -t claude-code -o cat | tail -n 5. Vous devez obtenir une ligne JSON par appel d’outil, la plus récente en dernier. Si rien ne s’affiche, le hook ne s’est pas exécuté. La section de dépannage ci-dessous explique comment diagnostiquer ce problème.
Ajoutez le même bloc sous PostToolUseFailure pour journaliser les appels qui ont échoué, car PostToolUse ne s’exécute qu’en cas de succès et une commande en échec est généralement l’information la plus utile. La raison d’utiliser logger plutôt que d’ajouter les entrées à un fichier de votre répertoire personnel tient à la propriété du fichier : un hook s’exécute avec le même utilisateur que le shell de l’agent. Tout ce que cet utilisateur peut modifier en ajoutant du contenu, il peut aussi le tronquer. Le journal est écrit par systemd-journald avec son propre compte.
Durée maximale d’exécution d’un hook
The data behind this chart
[
{
"label": "command, http or mcp_tool hook",
"default_timeout_seconds": 600
},
{
"label": "agent hook",
"default_timeout_seconds": 60
},
{
"label": "prompt hook",
"default_timeout_seconds": 30
},
{
"label": "command hook on UserPromptSubmit",
"default_timeout_seconds": 30
},
{
"label": "command hook on MessageDisplay",
"default_timeout_seconds": 10
},
{
"label": "any hook on SessionEnd",
"default_timeout_seconds": 1.5
}
]Par défaut, un hook de commande dispose de 600 secondes, soit dix minutes. Certains événements réduisent fortement cette durée. Les hooks SessionEnd partagent un budget de 1.5 secondes au total. Le nettoyage de fin de session doit donc être rapide. Toutefois, définir une valeur plus élevée pour timeout sur le hook augmente ce budget partagé en conséquence, jusqu’à 60 secondes.
Un hook qui atteint sa limite de temps est annulé et ne rend aucune décision. Pour une barrière de sécurité PreToolUse, cela signifie qu’il ne bloque pas l’opération : l’appel de l’outil se poursuit dans le flux normal de vérification des permissions. Gardez donc les scripts de barrière de sécurité courts. Pour les opérations lentes qui n’exigent pas d’attendre, comme l’envoi d’un journal ailleurs, définissez "async": true. Le hook s’exécute alors en arrière-plan sans retarder l’appel de l’outil.
Hooks, fichiers de règles, skills et serveurs MCP
Quatre éléments sont souvent confondus parce qu’ils modifient tous le comportement d’un agent. Un seul cesse d’être une simple suggestion.
Un fichier de règles (CLAUDE.md ou un fichier situé sous .claude/rules/) est du texte chargé dans le contexte du modèle. Il oriente son comportement, mais n’impose rien. Face à une longue conversation, à un diff volumineux et à une nouvelle demande, une de ses lignes peut être oubliée. C’est le mécanisme habituel qui explique pourquoi les agents ignorent les instructions que vous avez écrites.
Une skill est un dossier contenant des instructions et des scripts que le modèle charge lorsqu’il juge la skill pertinente. Ce jugement est précisément l’objectif d’une skill, mais aussi sa limite : le modèle garde la décision. Vous pouvez observer ces deux aspects avec une skill comme Ponytail, qui oriente l’agent vers la plus petite modification fonctionnelle : elle structure l’approche de toute une tâche d’une manière qu’aucun hook ne permet, mais uniquement lorsque le modèle choisit de la charger.
Un serveur MCP (model context protocol) fournit au modèle de nouveaux outils à appeler. Il élargit ce à quoi l’agent peut accéder. Il ne l’incite pas à utiliser ces outils. C’est aussi un processus distinct que vous devez exploiter, ce qui constitue une tâche à part entière : voir exécuter des serveurs MCP sur un VPS.
Un hook est le seul des quatre à s’exécuter sans que le modèle le choisisse. Utilisez un fichier de règles pour exprimer une préférence et une skill pour définir une procédure que le modèle doit suivre lorsqu’elle s’applique. Utilisez un hook pour l’étape qui doit avoir lieu à chaque fois ou pour l’action qui ne doit jamais avoir lieu. La comparaison détaillée, notamment les cas où une skill est préférable à un fichier de règles, se trouve dans la comparaison des skills, des serveurs MCP et des fichiers de règles.
Un plugin relève du packaging plutôt que d’un cinquième mécanisme. Il regroupe des hooks et des skills dans une unité installable. C’est ainsi qu’une équipe déploie la même protection sur chaque machine : voir le fonctionnement des plugins Claude Code.
La décision de sécurité sur un VPS partagé
Un hook est du code déclenché par l’agent. Il s’exécute avec les droits de l’utilisateur qui a lancé Claude Code. Il hérite de l’environnement et des permissions sur les fichiers de cet utilisateur. Sur un ordinateur portable, c’est une question de workflow. Sur un VPS où un agent s’exécute sans intervention, c’est une question de sécurité qui comporte quatre aspects pratiques.
Un hook présent dans un dépôt est du code que vous n’avez pas écrit. .claude/settings.json est versionné, donc le clonage d’un dépôt et le démarrage d’une session dans ce dépôt peuvent enregistrer les hooks fournis avec le dépôt. Claude Code soumet les hooks de projet à la boîte de dialogue de confiance de l’espace de travail pour ce dossier. Accepter la confiance est donc le moment où vous décidez de les exécuter. Lisez d’abord le bloc hooks.
Un hook voit l’intégralité de l’entrée de l’outil. Un hook d’audit qui journalise tool_input écrit dans un fichier chaque argument de chaque commande, y compris tout token qui se trouvait sur une ligne de commande. Ce journal doit alors bénéficier de la même protection que le secret. Cela fait partie du problème plus large consistant à tenir les secrets hors de portée d’un agent IA.
Un hook peut écrire dans le contexte du modèle. Tout ce qu’un hook SessionStart ou UserPromptSubmit écrit sur stdout est ajouté à la conversation. Un hook qui injecte du texte provenant d’une source externe, d’un issue tracker ou d’un fichier journal transmet au modèle du texte non fiable, comme si vous l’aviez saisi vous-même. Un hook qui relaie une note provenant d’une autre session Claude Code sur le même VPS fait la même chose. La sortie d’un agent ne mérite pas davantage votre confiance que celle de l’issue tracker. Traitez cette sortie standard comme une entrée, et non comme une sortie.
Le contrôle réel repose sur les privilèges. Exécutez l’agent avec un utilisateur dédié et non privilégié, en lui accordant uniquement les règles sudo dont il a besoin. Un refus PreToolUse est utile. Il est toutefois conçu comme une mesure de dernier recours : la documentation de référence indique la même chose à propos du filtre if et recommande d’utiliser le système de permissions lorsqu’un refus strict est nécessaire. Les règles de permission et le compte utilisé par le processus sont les éléments qui restent fiables sous pression.
Une propriété reste valable dans toutes les configurations. Les hooks PreToolUse sont exécutés avant la vérification du mode de permissions, quel que soit ce mode. Un hook qui renvoie deny bloque donc l’outil, même avec bypassPermissions. Les hooks peuvent restreindre ce que les règles de permission autorisent. Ils ne peuvent pas l’élargir.
Pourquoi mon hook ne se déclenche-t-il pas ?
Suivez ces étapes dans l’ordre. Chaque étape indique le symptôme que vous verrez réellement.
- Exécutez
/hookset vérifiez que le hook apparaît sous l’événement attendu. Un hook absent du menu indique généralement une erreur de syntaxe JSON dans le fichier de configuration, car les virgules finales et les commentaires ne sont pas autorisés, ou que le fichier ne se trouve dans aucun des six emplacements indiqués plus haut. - Comparez exactement le matcher avec le nom de l’outil. Les matchers sont sensibles à la casse.
"bash"ne correspond donc jamais à l’outilBash. - Exécutez le script manuellement avec un exemple d’entrée, comme dans l’exemple 1 ci-dessus. Un code de sortie inattendu indique un bug dans votre script. Claude Code le signale comme une erreur de hook et non comme une décision.
- Un message indiquant
jq: command not foundsignifie quejqest absent de cette machine. Uncommand not foundpour votre propre script signifie que le chemin n’a pas été résolu. Utilisez donc${CLAUDE_PROJECT_DIR}ou un chemin absolu. Si le script ne s’exécute jamais, il n’est probablement pas exécutable. - Le hook affiche un JSON valide, mais rien ne se passe. Un hook au format shell passe par
sh -c. Si votre profil shell affiche une bannière, celle-ci est ajoutée avant votre JSON. La sortie standard ne commence alors plus par{. Claude Code interprète donc l’ensemble comme du texte brut et ignore la décision. À la sortie 0, rien n’est signalé ailleurs que dans le journal de débogage. Entourez toutechodans votre profil afin qu’il ne s’exécute que dans les shells interactifs. - Le problème persiste : démarrez la session avec
claude --debug-file /tmp/claude.loget exécuteztail -f /tmp/claude.logdans un deuxième terminal. Le journal de débogage indique quels hooks ont correspondu, le code de sortie renvoyé par chacun et tout ce qu’ils ont écrit sur la sortie standard et la sortie d’erreur.
FAQ
Quelle est la différence entre un hook Claude Code et une instruction CLAUDE.md ?
Une instruction CLAUDE.md est du texte placé dans le contexte du modèle. Elle entre donc en concurrence avec la conversation et la requête en cours, et le modèle peut lui accorder plus ou moins de poids. Un hook est une commande shell que Claude Code exécute à un point précis de son cycle de vie. Il s’exécute donc à chaque occurrence de l’événement, quelle que soit la décision du modèle. Utilisez une instruction pour exprimer une préférence. Utilisez un hook pour une étape qui doit toujours être exécutée ou une action qui ne doit jamais l’être.
Comment empêcher Claude Code d’exécuter une commande shell donnée ?
Enregistrez un hook PreToolUse avec un matcher Bash qui lit la commande depuis .tool_input.command, écrit la raison sur stderr et se termine avec le code 2. Claude Code annule l’appel et affiche votre raison au modèle. Cela se produit avant la vérification du mode d’autorisation. Le refus reste donc effectif, même en mode bypassPermissions. La correspondance sur une chaîne de commande constitue un garde-fou, et non une frontière de sécurité, car la même commande peut être écrite d’une manière que le motif ne reconnaît pas. Complétez-la par des règles d’autorisation et par l’utilisation d’un compte non privilégié.
Mon hook affiche un JSON valide, mais rien ne se passe. Pourquoi ?
La cause la plus fréquente est votre profil shell. Un hook sans champ args s’exécute via sh -c. Certains profils affichent une bannière à chaque ouverture de shell, qui se retrouve sur stdout avant votre JSON. Comme la sortie ne commence alors plus par {, Claude Code la traite entièrement comme du texte brut et ignore la décision. Avec le code de sortie 0, rien n’est en outre signalé dans la transcription. Protégez toute commande echo de votre profil par un test qui vérifie que le shell est interactif. Confirmez ensuite la correction en lisant le journal de débogage depuis claude --debug-file /tmp/claude.log.
Est-il sûr d’exécuter des hooks Claude Code sur un serveur partagé ?
Les hooks s’exécutent avec les droits de l’utilisateur qui a démarré Claude Code. Ils disposent donc des mêmes permissions sur les fichiers et peuvent effectuer toutes les actions autorisées pour ce compte. Deux pratiques couvrent la plupart des risques : exécutez l’agent avec un compte dédié non privilégié et une politique sudo restrictive, puis lisez le bloc hooks de tout dépôt avant d’accepter sa boîte de dialogue de confiance de l’espace de travail, car les hooks du projet sont fournis dans .claude/settings.json. Définissez "disableAllHooks": true dans votre fichier de configuration si vous ne voulez exécuter aucun de ces hooks.