Un hook Claude Code est une commande shell qui s'exécute automatiquement à un moment précis d'une session : avant un outil, après une édition, au démarrage. Les types de hooks, comment les configurer dans settings.json, et cinq hooks prêts à copier.
Claude Code Hooks : un hook interceptant une ligne de code

Un hook Claude Code est une commande shell qui s'exécute automatiquement à un moment précis d'une session : avant qu'un outil tourne, après une édition de fichier, au démarrage, ou quand Claude vous attend. Là où un skill ou un subagent restent des collaborateurs intelligents mais faillibles, un hook est une garantie mécanique : il se déclenche à chaque fois, sans jugement, sans oubli. On les configure dans un fichier settings.json en associant un événement (comme PreToolUse ou PostToolUse), un filtre, et un script. Résultat : vous pouvez bloquer une commande dangereuse avant qu'elle parte, formater votre code après chaque édition, ou recevoir une notification quand Claude a besoin de vous. Voici ce qu'est un hook, les types qui comptent, comment les configurer, cinq hooks prêts à copier, et comment vous en servir comme garde-fous quand vous ne voulez pas casser votre prod.

C'est quoi un hook Claude Code ?

Un hook est un déclencheur automatique. Vous dites à Claude Code « quand tel événement se produit, exécute tel script », et il le fait, à chaque fois, sans que Claude ait son mot à dire. C'est la différence fondamentale avec le reste de l'écosystème : un skill et un subagent dépendent du jugement du modèle, un hook est déterministe. Il ne décide pas s'il faut agir, il agit.

Techniquement, un hook est une commande shell (Claude Code accepte aussi des endpoints HTTP ou des prompts LLM, mais la commande shell est le cas courant et celui qu'on couvre ici) qui s'exécute à un point précis du cycle de vie de la session. Quand l'événement se déclenche et que votre filtre correspond, Claude Code envoie au script un objet JSON décrivant ce qui se passe, sur l'entrée standard. Votre script inspecte cette donnée, agit, et peut renvoyer une décision : laisser faire, ou bloquer.

Concrètement, la boucle est toujours la même. Un événement se produit. Claude Code regarde si un hook y est associé et si son filtre correspond. Si oui, il lance votre script en lui passant, sur l'entrée standard, un JSON qui contient le contexte : l'identifiant de session, le nom de l'outil concerné, ses arguments (par exemple la commande shell ou le chemin du fichier édité), le répertoire de travail. Votre script lit ce JSON, en extrait ce qui l'intéresse, fait son travail, puis communique le résultat par deux canaux : son code de sortie et ce qu'il écrit sur la sortie standard. C'est ce couple (code de sortie + sortie) qui dit à Claude Code s'il doit continuer, bloquer, ou ignorer. Tout le système tient dans cette mécanique, et une fois qu'on l'a en tête, écrire un hook devient trivial.

Pourquoi un founder devrait s'y intéresser ? Parce que c'est le seul mécanisme de Claude Code qui vous donne des garanties, pas des probabilités. Vous pouvez demander gentiment à Claude, via un CLAUDE.md, de « ne jamais supprimer de fichiers sans confirmation ». Il le respectera la plupart du temps. « La plupart du temps » n'est pas une politique de sécurité. Un hook, lui, bloque l'action à tous les coups. Quand on build seul et qu'une commande de travers peut effacer une base ou pousser un secret, cette différence entre « la plupart du temps » et « à tous les coups » vaut de l'or.

Un hook sert deux grandes familles d'usages : automatiser (formater, linter, journaliser, notifier, sans y penser) et sécuriser (bloquer les commandes dangereuses, empêcher l'édition de fichiers sensibles). Les deux reposent sur le même mécanisme. La première vous fait gagner du temps sur les tâches répétitives que vous oubliez de faire ; la seconde vous protège des erreurs que vous n'auriez pas vu venir. Un founder solo a besoin des deux : personne d'autre ne va formater le code à votre place, et personne d'autre ne va rattraper la commande de travers avant qu'elle parte.

Le cycle de vie d'un hook : de l'événement à la décision

Les types de hooks : PreToolUse, PostToolUse, Notification, SessionStart

Claude Code déclenche des hooks à de nombreux moments, mais quatre événements couvrent l'essentiel de ce dont vous avez besoin. On peut les ranger par cadence.

Une fois par session :

  • SessionStart : au démarrage ou à la reprise d'une session. Le moment idéal pour injecter du contexte (l'état de votre git, une note de projet) que Claude verra dès le départ.

À chaque appel d'outil, dans la boucle de travail de Claude :

  • PreToolUse : juste avant qu'un outil s'exécute. C'est le seul moment où vous pouvez bloquer une action avant qu'elle arrive. Le garde du corps.
  • PostToolUse : juste après qu'un outil a réussi. Le moment pour réagir : formater le fichier qui vient d'être édité, lancer un lint, journaliser.

Au fil de la session :

  • Notification : quand Claude Code envoie une notification, typiquement quand il attend votre input ou une permission. Le moment pour vous prévenir (son, message) que Claude a besoin de vous.

Ces quatre-là suffisent pour 90% des besoins d'un founder. Mais la liste réelle est bien plus longue, et quelques autres événements méritent d'être connus dès maintenant, parce qu'ils débloquent des usages malins :

  • UserPromptSubmit : au moment où vous soumettez un prompt, avant que Claude le traite. Sa sortie est ajoutée au contexte, donc c'est l'endroit pour injecter des infos à jour à chaque tour, ou pour bloquer un prompt qui ne respecte pas une règle.
  • Stop : quand Claude termine sa réponse. Le moment idéal pour lancer votre suite de tests ou un check final, automatiquement, dès que Claude a fini de coder.
  • SubagentStop : quand un subagent finit son travail. Utile pour réagir au résultat d'une délégation.
  • PreCompact : juste avant que Claude Code compacte le contexte (quand la conversation devient trop longue). Pour sauvegarder un état avant que du contexte soit résumé.
  • SessionEnd : à la fermeture de la session. Pour nettoyer, archiver un log, fermer proprement.

Il en existe encore d'autres (permissions, notifications d'outils MCP, changement de répertoire), mais vous les découvrirez au besoin. Commencez par les quatre du haut, ajoutez Stop et UserPromptSubmit quand vous voulez automatiser vos tests et votre contexte. C'est déjà un setup très complet.

Les quatre types de hooks : SessionStart, PreToolUse, PostToolUse, Notification

Configurer un hook dans settings.json (structure minimale)

Les hooks se configurent en JSON, dans un fichier settings.json. Trois emplacements, selon la portée voulue :

Fichier Portée Partagé
~/.claude/settings.json Tous vos projets Non, local à votre machine
.claude/settings.json Ce projet Oui, commitable dans le repo
.claude/settings.local.json Ce projet Non, gitignored

Pour un garde-fou d'équipe (bloquer les commandes dangereuses sur ce repo), utilisez .claude/settings.json et commitez-le : tout le monde en profite. Pour un réglage perso (une notification quand Claude vous attend), ~/.claude/settings.json.

La structure est toujours la même. Un objet hooks, une clé par événement, et pour chaque événement une liste de groupes. Chaque groupe a un matcher (le filtre) et une liste de hooks (les scripts à lancer). Le squelette minimal :

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "/chemin/vers/mon-script.sh"
          }
        ]
      }
    ]
  }
}

Ce hook-là dit : après chaque édition ou écriture de fichier (Edit|Write), lance mon-script.sh. C'est tout. Trois notions à comprendre pour aller plus loin : le matcher, le champ if, et les codes de sortie.

Avant ça, une question revient toujours : qu'est-ce que mon script reçoit, exactement ? Sur l'entrée standard, un JSON. Pour un événement d'outil, il ressemble à ça :

{
  "session_id": "abc123",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf /tmp/build" },
  "cwd": "/home/vous/projet"
}

Votre script y pioche ce dont il a besoin avec jq. Pour récupérer la commande shell : jq -r '.tool_input.command'. Pour le chemin d'un fichier édité : jq -r '.tool_input.file_path'. C'est la seule chose à savoir pour lire le contexte d'un événement, et c'est ce que font tous les scripts plus bas. Le nom exact des champs varie selon l'événement (un SessionStart ne reçoit pas les mêmes données qu'un PreToolUse), mais le principe ne change jamais : du JSON sur stdin, que vous lisez avec jq.

Anatomie d'un hook dans settings.json

Les matchers : filtrer finement

Le matcher décide quand un groupe de hooks se déclenche. Pour les événements liés à un outil, il matche le nom de l'outil :

  • "Bash" : uniquement les commandes shell.
  • "Edit|Write" : les éditions et écritures de fichiers (la barre verticale est un « ou »).
  • "*" ou pas de matcher : à chaque occurrence de l'événement.

Certains événements (comme UserPromptSubmit ou Stop) ne prennent pas de matcher : ils se déclenchent toujours. Un matcher ajouté là est simplement ignoré.

Pour filtrer encore plus finement, il y a le champ if, qui regarde non seulement l'outil mais ses arguments, en syntaxe de règle de permission. "Bash(rm *)" ne se déclenche que si la commande shell est un rm. "Edit(*.ts)" ne réagit qu'aux fichiers TypeScript. C'est ce qui vous évite de lancer un script à chaque commande alors que vous ne visez qu'un cas précis, et ça économise le coût de lancer un process pour rien.

Les matchers : filtrer quels outils déclenchent un hook

Les codes de sortie à connaître

C'est le point que tout le monde rate, et pourtant c'est le cœur du système. Le code de sortie de votre script dit à Claude Code quoi faire :

  • Exit 0 : succès. Pour la plupart des événements, ce que votre script écrit part dans le journal de debug (les exceptions utiles étant SessionStart et UserPromptSubmit, où la sortie est ajoutée au contexte que Claude voit).
  • Exit 2 : erreur bloquante. C'est le code qui compte. Sur un PreToolUse, il bloque l'appel d'outil, et le texte de l'erreur (stderr) est renvoyé à Claude pour qu'il comprenne pourquoi.
  • Tout autre code : erreur non bloquante. L'action continue quand même.

Retenez ce piège, parce qu'il casse la moitié des premiers hooks : exit 1 ne bloque rien. Par réflexe Unix, on met exit 1 pour signaler une erreur. Ici, seul exit 2 bloque. Si votre hook est censé faire respecter une règle, c'est exit 2, sinon Claude passe outre.

Pour un contrôle plus fin qu'un simple « bloque / laisse passer », vous pouvez, sur un exit 0, écrire un JSON structuré sur la sortie standard. C'est ce que fait le hook block-rm plus bas : au lieu de compter sur le code de sortie, il renvoie un objet avec un permissionDecision. Ce champ accepte trois valeurs qui couvrent tous les cas : "deny" refuse l'action et donne la raison à Claude, "allow" l'autorise sans redemander la permission, et "ask" force le passage par la demande de permission habituelle. Ce JSON est plus expressif que les codes de sortie, parce qu'il vous laisse non seulement trancher, mais expliquer pourquoi, un message que Claude lit et comprend. Pour un PreToolUse, retenez la règle : le code de sortie 2 est la façon rapide de bloquer, le JSON permissionDecision est la façon propre de bloquer avec une raison.

Les codes de sortie d'un hook et leur effet sur PreToolUse

5 hooks prêts à copier

Assez de théorie. Voici cinq hooks concrets, chacun grounded sur le mécanisme réel de Claude Code. Copiez le JSON dans votre settings.json, le script dans .claude/hooks/ (pensez à le rendre exécutable avec chmod +x), adaptez le chemin, et ça tourne. Chacun couvre un besoin réel de founder-dev : un pour se protéger, un pour gagner du temps, un pour ne rien rater, un pour démarrer bien informé, un pour garder une trace.

Cinq hooks prêts à copier

Bloquer les commandes dangereuses (rm -rf)

Le garde-fou numéro un. Un hook PreToolUse qui intercepte tout rm et refuse s'il contient rm -rf. La config, dans .claude/settings.json :

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

Le script block-rm.sh lit la commande sur l'entrée standard et refuse si elle est destructrice :

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Commande destructrice bloquée par un hook"
    }
  }'
else
  exit 0
fi

Quand Claude tente un rm -rf, le hook renvoie une décision deny, l'appel est bloqué, et Claude voit la raison. Quand c'est un rm fichier.txt inoffensif, le script fait exit 0 et laisse le flux de permission normal s'appliquer. Notez la nuance importante : le hook peut refuser, mais rester silencieux n'approuve pas. Il bloque ou il se tait, il ne valide jamais à votre place.

Auto-format Prettier après édition

Le hook qui vous fait gagner du temps tous les jours. Un PostToolUse sur Edit|Write qui passe Prettier sur chaque fichier que Claude touche, pour que votre code reste formaté sans y penser :

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh"
          }
        ]
      }
    ]
  }
}

Le script récupère le chemin du fichier édité depuis le JSON et le formate :

#!/bin/bash
FILE=$(jq -r '.tool_input.file_path')

if [ -n "$FILE" ] && [ -f "$FILE" ]; then
  npx prettier --write "$FILE"
fi
exit 0

Plus jamais de « oups, j'ai oublié de formater ». Le hook s'en charge, à chaque édition, sans exception. La même structure marche pour tout ce qui doit tourner après une édition : remplacez prettier par eslint --fix pour linter, par black si vous êtes en Python, ou enchaînez plusieurs commandes. Le point clé, c'est le matcher Edit|Write qui cible précisément le moment où un fichier vient de changer.

Notification quand Claude attend votre input

Vous lancez Claude sur une tâche longue, vous partez faire autre chose, et vous revenez dix minutes trop tard parce qu'il attendait une permission depuis le début. Un hook Notification règle ça en vous prévenant activement :

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude a besoin de vous\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Sur macOS, ça déclenche une notification système. Sur Linux, remplacez par notify-send "Claude Code" "Claude a besoin de vous". Vous ne surveillez plus le terminal, il vous appelle quand il faut. Ce hook change la façon de travailler avec Claude sur les tâches longues : vous lancez, vous partez sur autre chose, et vous revenez pile au moment où il vous faut, au lieu de faire des allers-retours pour vérifier s'il a fini ou s'il est bloqué. Pour un founder qui jongle entre le code et dix autres casquettes, récupérer ces minutes perdues à surveiller un terminal n'est pas anodin.

Injecter du contexte au démarrage

Un hook SessionStart dont la sortie est ajoutée au contexte que Claude voit dès le début. Pratique pour lui donner l'état du projet sans le taper à chaque fois :

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Branche: $(git branch --show-current) | Derniers commits: $(git log --oneline -3)\""
          }
        ]
      }
    ]
  }
}

À chaque session, Claude démarre en sachant sur quelle branche vous êtes et ce qui a été fait récemment. C'est l'un des rares événements (avec UserPromptSubmit) où la sortie du hook nourrit vraiment le contexte de Claude, pas juste le journal de debug. Vous pouvez pousser plus loin : injecter les tickets ouverts, la liste des tâches en cours, l'URL de staging, une convention à rappeler. Tout ce que vous retapez au début de chaque session est un bon candidat. Attention à rester concis, ce contexte occupe de la place : mettez l'essentiel, pas un roman.

Journaliser les commandes pour audit

Le cinquième, discret mais précieux quand vous voulez une trace. Un PreToolUse sur Bash qui écrit chaque commande dans un fichier avant exécution :

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/bash-audit.log"
          }
        ]
      }
    ]
  }
}

Vous gardez un journal de tout ce que Claude a lancé dans votre shell. Le jour où quelque chose déraille, vous savez exactement quoi. Et comme le script ne renvoie pas d'exit 2, il n'interfère jamais avec l'exécution : il observe, il ne bloque pas. Pour un log plus exploitable, ajoutez un horodatage à chaque ligne, par exemple en préfixant la date : echo "$(date -Iseconds) $(jq -r '.tool_input.command')" >> ~/.claude/bash-audit.log. Vous obtenez une trace chronologique complète, précieuse pour un post-mortem ou simplement pour comprendre comment Claude a résolu une tâche.

Hooks comme garde-fous prod (angle founder)

Voici ce qui sépare un founder qui dort tranquille d'un founder qui checke son terminal en panique. Quand vous laissez un agent IA agir sur votre machine, la question n'est pas « est-ce que ça va bien se passer », c'est « qu'est-ce qui se passe le jour où ça se passe mal ». Les hooks sont votre réponse à cette question, parce qu'ils transforment des règles que vous espérez respectées en règles imposées.

Trois garde-fous que je mettrais en place avant de laisser Claude toucher quoi que ce soit de sérieux :

  1. Bloquer les commandes destructrices. Le hook rm -rf ci-dessus, étendu à ce qui fait peur chez vous : git push --force qui écrase l'historique distant, un DROP TABLE sur votre base, une suppression de bucket, un docker system prune. La logique ne change pas : un PreToolUse sur Bash, et dans le script un grep sur les motifs interdits qui renvoie une décision deny. Vous pouvez tout regrouper dans un seul hook « liste noire » qui refuse une poignée de commandes que vous ne voulez voir passer sous aucun prétexte. L'action ne part jamais, et vous n'avez pas eu à surveiller.
  2. Protéger les fichiers sensibles. Un PreToolUse sur Edit|Write qui refuse toute écriture dans .env, dans vos fichiers de secrets ou vos migrations de prod. Claude peut coder tant qu'il veut, il ne touchera pas ce que vous avez verrouillé. Le script tient en quelques lignes :
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path')

case "$FILE" in
  *.env|*/secrets/*|*/migrations/prod/*)
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Fichier protégé: édition interdite par un hook"
      }
    }'
    ;;
  *)
    exit 0
    ;;
esac

Branché sur un PreToolUse avec matcher "Edit|Write", ce hook rend vos fichiers critiques intouchables par l'agent, sans vous empêcher de les éditer vous-même. C'est le genre de verrou qu'on met une fois et qu'on oublie, jusqu'au jour où il vous sauve d'un secret réécrit ou d'une migration de prod modifiée par erreur. 3. Garder une trace. Le hook d'audit. Quand vous devez comprendre après coup ce qui s'est passé, un log de toutes les commandes vaut mille suppositions.

Le principe de fond, et c'est ce qui rend les hooks si adaptés à un founder solo : ils déplacent la sécurité du bon vouloir vers la mécanique. Vous n'avez plus besoin de faire confiance au modèle pour respecter une consigne. Vous n'avez plus besoin de relire chaque commande avant qu'elle parte. Vous posez la règle une fois, dans un fichier, et elle tient. C'est exactement le genre de filet qu'on installe quand on outille des founders : automatiser ce qui doit l'être, verrouiller ce qui ne doit jamais casser, et libérer sa tête pour le reste.

Un dernier conseil d'usage, parce qu'un hook mal fait peut se retourner contre vous. Un hook PreToolUse s'exécute avant chaque appel d'outil concerné : s'il est lent, il ralentit toute votre session. Gardez vos scripts rapides et ciblés, servez-vous du champ if pour ne les déclencher que sur les cas qui comptent, et testez-les à part avant de les brancher (un hook qui plante avec un exit 2 par erreur bloquera Claude sans raison). Commencez par un seul hook, celui qui vous protège le plus (le blocage des commandes destructrices), vérifiez qu'il fait bien son travail, puis ajoutez les autres un par un. Un garde-fou que vous comprenez vaut mieux que dix que vous avez copiés sans tester.

Une session Claude Code sans hook, c'est une voiture rapide sans ceinture. Ça roule très bien, jusqu'au jour où non.

Hooks comme garde-fous prod : trois verrous à poser

Pour aller plus loin

FAQ

Qu'est-ce qu'un hook dans Claude Code ?

Un hook est une commande shell qui s'exécute automatiquement à un moment précis d'une session Claude Code : avant un outil (PreToolUse), après (PostToolUse), au démarrage (SessionStart), ou lors d'une notification. On le configure dans un fichier settings.json. Contrairement à une consigne donnée à Claude, un hook est déterministe : il se déclenche à chaque fois, sans dépendre du jugement du modèle.

Quelle est la différence entre PreToolUse et PostToolUse ?

PreToolUse se déclenche avant qu'un outil s'exécute : c'est le seul moment où vous pouvez bloquer une action (en renvoyant une décision deny ou un code de sortie 2). PostToolUse se déclenche après qu'un outil a réussi : c'est le moment pour réagir, par exemple formater le fichier qui vient d'être édité ou lancer un lint. L'un prévient, l'autre nettoie.

Comment configurer un hook dans settings.json ?

Ajoutez un objet hooks à votre settings.json, avec une clé par événement. Chaque événement contient une liste de groupes, chacun avec un matcher (le filtre, par exemple "Bash" ou "Edit|Write") et une liste de hooks de type command. Placez le fichier dans .claude/settings.json pour un réglage partagé avec l'équipe, ou ~/.claude/settings.json pour un réglage perso.

Comment bloquer une commande dangereuse avec un hook ?

Utilisez un hook PreToolUse avec un matcher "Bash" et un champ if ciblé comme "Bash(rm *)". Le script lit la commande sur l'entrée standard et renvoie une décision permissionDecision: "deny" (ou fait exit 2) si la commande est dangereuse. Claude Code bloque alors l'appel et montre la raison à Claude. Attention : exit 1 ne bloque pas, seul exit 2 (ou une décision deny) le fait.

À quoi servent vraiment les hooks pour un founder ?

À deux choses : automatiser (formater, linter, journaliser, notifier sans y penser) et sécuriser (bloquer les commandes destructrices, protéger les fichiers sensibles). Leur vrai atout est la garantie : là où une consigne dans un CLAUDE.md est respectée « la plupart du temps », un hook s'applique à tous les coups. C'est ce qui permet de laisser un agent IA agir sans surveiller chaque commande.

Les hooks ralentissent-ils Claude Code ?

Ils peuvent, si vous les écrivez mal. Un hook PreToolUse s'exécute avant chaque appel d'outil qui correspond à son matcher : s'il est lent, il ajoute ce délai à chaque action. La parade est simple : gardez vos scripts rapides, ciblez avec le champ if pour ne les déclencher que sur les cas utiles, et évitez de lancer un hook sur "*" (tous les outils) si un matcher précis suffit. Un hook bien ciblé est imperceptible.

Peut-on utiliser les hooks avec les serveurs MCP ?

Oui. Les outils exposés par vos serveurs MCP apparaissent comme des outils normaux dans les événements comme PreToolUse et PostToolUse, sous la forme mcp__serveur__outil. Vous pouvez donc les matcher : mcp__github__.* cible tous les outils du serveur GitHub, par exemple pour journaliser ou valider chaque opération. Le point d'attention : pour matcher tous les outils d'un serveur, ajoutez bien .* après son nom, sinon le matcher est comparé comme une chaîne exacte et ne matche rien.