TIPS 05 · GUARDIA

Le gardien

Deux hooks pour Claude Code : l'un vous demande confirmation avant qu'une commande supprime des fichiers et bloque les irréparables, l'autre lance les tests après chaque modification.

Ce que c'est et pourquoi ça compte

Voici les deux hooks que j'utilise dans mes projets. Un hook est une commande que Claude Code lance tout seul à un moment précis de la session. La différence avec une instruction écrite dans le chat ou dans le CLAUDE.md tient en ceci : une instruction, Claude peut la perdre en route, après deux heures de travail. Un hook, non. Claude Code l'exécute à chaque fois, que Claude s'en souvienne ou pas.

Le gardien sert parce que certaines commandes ne s'annulent pas : elles suppriment des fichiers sans passer par la corbeille, réécrivent l'historique git, vident une table de la base de données. Il suffit d'un mauvais chemin ou d'une demande mal comprise.

Mon gardien fait deux choses. Si une commande supprime des fichiers, il vous demande confirmation, même en mode auto, celui où Claude travaille sans rien vous demander. Si elle est irréparable, comme effacer tout le disque ou forcer un push, il la bloque, point.

Les tests après chaque modification servent pour l'autre problème classique : Claude change un fichier, vous dit « fini », et entre-temps il a cassé quelque chose ailleurs. Avec le hook, le résultat des tests lui arrive tout de suite, pendant qu'il a encore en tête ce qu'il a touché.

1. Le gardien : avant chaque commande

Le premier hook utilise l'événement PreToolUse, qui se déclenche avant que Claude n'utilise un outil. Avec le filtre « Bash », il ne regarde que les commandes du terminal. Créez le fichier .claude/hooks/gardien.sh dans le dossier du projet et collez-y ceci :

Le fichier
#!/bin/bash
# gardien.sh : demande avant de supprimer, bloque l'irréparable
command -v jq >/dev/null || { echo "Bloqué : jq manquant (brew install jq)" >&2; exit 2; }
CMD=$(jq -er '.tool_input.command') || { echo "Bloqué : entrée illisible" >&2; exit 2; }
NO='rm[[:space:]]+-[a-z]*r[a-z]*[[:space:]]+(/|~/?|\$HOME/?)\*?([[:space:]]|$)|git[[:space:]]+push([[:space:]].*)?[[:space:]](-f|--force)|git[[:space:]]+reset[[:space:]]+--hard|drop[[:space:]]+(table|database)'
if grep -Eiq "$NO" <<<"$CMD"; then echo "Bloqué : commande irréparable ($CMD)" >&2; exit 2; fi
ASK='^[[:space:]]*((sudo|xargs)[[:space:]]+)*(rm|rmdir|unlink)([[:space:]]|$)|^[[:space:]]*find[[:space:]].*[[:space:]]-delete'
if tr ';&|()`' '\n\n\n\n\n\n' <<<"$CMD" | grep -Eq "$ASK"; then
  jq -n --arg r "Le gardien : cette commande supprime des fichiers. Confirmez-vous ?" \
    '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: $r}}'
fi
exit 0

Comment ça marche : avant chaque commande, Claude Code passe la commande au script en JSON, dans le champ tool_input.command. Le script peut répondre de trois façons :

  • Il se termine avec le code 2 : la commande ne part pas et Claude lit la raison. C'est le cas des commandes de la liste NO.
  • Il écrit un petit JSON avec permissionDecision « ask » et se termine avec 0 : Claude Code vous affiche la demande de permission, avec la raison écrite par le script, et la commande ne part que si vous dites oui. C'est le cas des commandes de la liste ASK.
  • Il se termine avec 0 sans rien écrire : la commande continue avec les permissions normales.

Les deux lignes après le commentaire bloquent tout si jq, le programme qui lit le JSON, manque, ou si l'entrée ne se lit pas. Sur Mac, vous installez jq avec brew install jq.

Ce qu'il demande, ce qu'il bloque, ce qu'il laisse passer

  • ASK, demande confirmation : rm, rmdir, unlink et find … -delete, même avec sudo ou xargs devant. Avant de chercher, le script coupe la commande sur ; & | ( ) et les backticks, ce qui lui permet d'attraper aussi le rm à la fin de cd dossier && rm -r build.
  • NO, bloque, point : rm -r sur /, ~ ou $HOME, c'est-à-dire tout le disque ou tout le dossier utilisateur ; git push avec -f ou --force ; git reset --hard ; DROP TABLE et DROP DATABASE, en majuscules ou en minuscules.
  • Laisse passer : git rm --cached, docker rm et tout le reste, parce que là, la commande est git ou docker, pas rm.

Les deux listes, c'est à vous de les allonger. Par exemple git clean, qui supprime les nouveaux fichiers que git ne connaît pas encore : il n'est pas dans ma version. Si vous l'utilisez, mettez-le dans ASK ou dans NO, avec les commandes qui font des dégâts dans votre projet.

Ce qu'il ne voit pas

Le gardien ne regarde que les commandes Bash. Si Claude vide un fichier en le réécrivant avec l'outil Write, il ne l'arrête pas. Pour les fichiers qui ne doivent jamais changer, comme .env, il faut une règle deny dans les permissions, par exemple Edit(./.env), qui vaut pour tous les outils qui modifient des fichiers. Ou un second hook sur Write.

2. Les tests : après chaque modification

Le second hook utilise PostToolUse, qui se déclenche après qu'un outil a fini son travail. Avec le filtre « Edit|Write », il part chaque fois que Claude modifie ou crée un fichier. Créez .claude/hooks/test-apres-modif.sh :

Le fichier
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(npm test 2>&1) && exit 0
echo "Tests en échec après la modification :" >&2
echo "$OUT" | tail -n 30 >&2
exit 2

Mettez votre propre commande de tests à la place de npm test. Si les tests passent, vous ne voyez rien. S'ils échouent, le script envoie à Claude les 30 dernières lignes de l'erreur et se termine avec le code 2.

Ici, le code 2 n'annule pas la modification : elle est déjà faite. Il fait lire l'erreur à Claude, qui la corrige tout de suite. C'est la différence entre les deux hooks : le gardien peut dire non avant, celui-ci peut seulement prévenir après. Et Claude attend la fin des tests après chaque modification : si les vôtres sont lents, ne mettez dans le script que les rapides.

3. Activez-les

Rendez les deux scripts exécutables. Depuis le terminal, dans le dossier du projet :

La ligne
chmod +x .claude/hooks/*.sh

Ensuite, dans .claude/settings.json, toujours dans le dossier du projet, vous déclarez les deux hooks :

Le fichier
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/gardien.sh" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-apres-modif.sh" }] }
    ]
  }
}

Si le fichier existe déjà, ne le remplacez pas : ajoutez la partie « hooks » à côté de ce qu'il contient, et vérifiez les virgules. $CLAUDE_PROJECT_DIR, c'est le dossier depuis lequel vous avez ouvert la session : Claude Code trouve ainsi les scripts même quand Claude travaille dans un sous-dossier.

Si vous n'avez pas envie de tout faire à la main, demandez-le à Claude :

Le prompt
Crée dans ce projet les deux hooks de ce guide : un PreToolUse sur Bash qui me demande confirmation avant de supprimer des fichiers et bloque avec le code 2 les commandes irréparables, et un PostToolUse sur Edit et Write qui lance [ta commande de tests] après chaque modification. Déclare-les dans .claude/settings.json sans rien enlever de ce qui s'y trouve déjà. Ensuite, montre-moi les deux scripts et explique-moi ce que fait chaque ligne.

4. Le test

D'abord, je teste le gardien tout seul, sans Claude. Depuis le terminal, dans le dossier du projet, je lui passe une fausse commande, comme le ferait Claude Code :

La ligne
echo '{"tool_input":{"command":"rm -rf essai"}}' | .claude/hooks/gardien.sh; echo $?

Vous devez voir un petit JSON avec « ask », puis 0. Avec git reset --hard à la place de rm -rf essai, vous devez voir « Bloqué » puis 2. Avec ls, seulement 0. Le script ne fait que lire le texte : rien n'est exécuté ici. Ce test compte, parce que si le chemin dans settings.json est faux ou si chmod +x manque, Claude Code affiche seulement un avertissement « hook error » et la commande passe sans question.

Ensuite, le vrai test. Ouvrez une nouvelle session : en général, Claude Code remarque tout seul les changements dans settings.json, mais comme ça vous êtes sûr. Tapez /hooks et vérifiez que les deux sont là, l'un sous PreToolUse et l'autre sous PostToolUse. Puis demandez à Claude :

Le prompt
Crée un dossier vide appelé test-gardien, puis supprime-le.

La demande de permission doit apparaître, avec la raison « Le gardien : cette commande supprime des fichiers » et l'étiquette [settings], qui vous dit d'où vient la question. La première fois, répondez non et vérifiez que le dossier est toujours là.

Pour le hook des tests :

Le prompt
Modifie une ligne dans un fichier du projet pour qu'un test échoue, puis dis-moi ce que tu as reçu après la modification. À la fin, remets tout comme avant et vérifie que les tests passent.

Les erreurs les plus courantes

  • Dire oui par habitude : la question du gardien est là exprès. Lisez la commande avant de répondre.
  • Faire confiance au gardien sans l'avoir vu travailler : avec un mauvais chemin, la commande passe avec un simple avertissement. Faites toujours le test à la main avec echo.
  • Partir avec des tests déjà cassés : chaque modification reçoit une erreur qui n'a rien à voir. Avant d'activer le hook, faites passer les tests, ou ne lancez que ceux qui passent.

Check-list

  • gardien.sh dans .claude/hooks, avec vos listes ASK et NO
  • test-apres-modif.sh avec votre commande de tests
  • jq installé
  • chmod +x sur les deux scripts
  • Hooks déclarés dans .claude/settings.json
  • Test à la main avec echo : ask pour rm, 2 pour git reset --hard
  • Test dans Claude Code : demande de permission pour le dossier, test en échec signalé

Questions fréquentes

Et si personne ne répond la nuit ?

La commande reste en attente sur la demande jusqu'à votre retour, et rien n'est supprimé. C'est vrai aussi en mode auto. Si en revanche vous lancez Claude Code sans personne devant, par exemple avec claude -p, la demande ne peut pas s'afficher : Claude Code refuse la commande et Claude lit la raison.

Le gardien me protège-t-il de tout ?

Non. Il regarde le texte des commandes Bash, et on peut faire les mêmes dégâts autrement, par exemple avec un petit script Python ou en réécrivant un fichier avec Write. Il réduit le risque. Le vrai filet reste git : un commit avant les longs travaux et une copie du projet en ligne.

← Tous les guides