Implémenter un hook
Sur cette page
Passons de la théorie au code. Nous allons construire un hook qui empêche Claude de lire ou de modifier les fichiers .env et tout ce qui se trouve dans un dossier secrets/. Il met en œuvre l’intégralité du mécanisme décrit à la leçon précédente : un matcher, un script qui lit stdin, un code de sortie qui tranche. Nous terminerons par la question qu’il faut toujours se poser : ce hook était-il nécessaire, ou une règle de permission suffisait-elle ?
Configurer le hook
Créez le dossier .claude/hooks/ à la racine du projet, puis ajoutez à .claude/settings.json :
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write|Grep",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-secrets.js"]
}
]
}
]
}
}
Trois choix méritent une explication. Le matcher couvre les trois outils de fichiers et Grep : une clé d’API ne fuit pas seulement quand on ouvre le fichier, elle fuit aussi quand une recherche de motif en affiche la ligne (sur macOS et Linux, Grep n’est présent que si vous l’activez, mais le nommer ne coûte rien). La commande est écrite en forme exec, command plus args : Claude Code lance node directement, sans shell, et remplace ${CLAUDE_PROJECT_DIR} par la racine du projet, quel que soit le répertoire courant au moment de l’appel. C’est la forme recommandée dès qu’un chemin est en jeu, et elle rend le hook partageable : commité dans .claude/settings.json, il fonctionne chez chaque collègue sans adaptation.
Implémenter le script
Créez .claude/hooks/protect-secrets.js :
async function main() {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const event = JSON.parse(Buffer.concat(chunks).toString());
// Chemin visé : file_path pour Read/Edit/Write, path pour Grep
const target = (event.tool_input?.file_path || event.tool_input?.path || "")
.replace(/\\/g, "/"); // Windows : normaliser les séparateurs
const protectedPatterns = ["/.env", "/secrets/"];
const hit = protectedPatterns.find((p) => target.includes(p));
if (hit) {
console.error(
`Accès refusé : ${target} correspond au motif protégé "${hit}". ` +
"Lis la variable d'environnement correspondante plutôt que le fichier."
);
process.exit(2);
}
}
main();
Quatre moments structurent ce script. Il commence par accumuler l’intégralité de stdin avant de l’analyser, parce que les données arrivent par fragments et qu’un JSON.parse sur un fragment échouerait. Il extrait le chemin visé en essayant file_path puis path, les outils ne nommant pas leur argument de la même façon, et normalise les barres obliques inverses pour que la comparaison tienne sous Windows. Il compare ensuite à une liste de motifs : parce que les chemins arrivent absolus, /.env attrape .env à n’importe quelle profondeur sans attraper environment.ts. S’il y a correspondance, il écrit une explication sur stderr et sort avec le code 2 : Claude reçoit le message et comprend que l’opération a été bloquée, avec une alternative. Sinon, la fonction se termine normalement, le processus sort avec 0, et l’appel suit son cours. Le silence vaut absence d’objection.
Tester le hook
Les hooks des fichiers de réglages sont relus par Claude Code quand le fichier change ; si vous veniez de créer le dossier .claude/, relancez la session par prudence. Vérifiez avec /hooks que le hook apparaît sous PreToolUse, puis demandez :
Lis le contenu du fichier .env
Claude tente l’opération Read, le hook l’intercepte, et Claude vous explique qu’il n’a pas pu lire le fichier, souvent en proposant de lire la variable d’environnement. Testez aussi une modification (« ajoute une ligne à .env ») et une recherche (« cherche DATABASE_URL dans le projet ») : la même protection s’applique, ce qui valide les autres termes du matcher. Pour voir le détail d’un hook qui ne se comporte pas comme prévu, lancez claude --debug et lisez ~/.claude/debug/<session-id>.txt : chaque exécution y laisse une ligne avec son code de sortie et sa durée.
Ce que le hook ne couvre pas
Testez maintenant : « Affiche .env avec cat. » L’appel passe par l’outil Bash, que notre matcher ne surveille pas, et le fichier s’affiche. C’est la limite de tout hook de ce type : il ne voit que les outils qu’il nomme. Vous pourriez ajouter Bash au matcher et analyser tool_input.command, mais reconnaître toutes les façons de lire un fichier dans un shell est un puits sans fond.
Pour ce besoin précis, la solution intégrée est meilleure. Une règle de permission deny sur Read s’applique aux outils de fichiers, aux mentions @, et aux commandes de lecture que Claude Code reconnaît dans Bash (cat, head, tail, sed, les redirections) ; elle bloque aussi Edit et Write sur le même chemin, dans tous les modes de permission :
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)"]
}
}
Elle ne couvre pas un script Python ou Node qui ouvrirait le fichier lui-même ; pour une protection au niveau du système, c’est le bac à sable (sandboxing) qui bloque tous les processus, avec son réglage sandbox.credentials dédié aux fichiers d’identifiants. Le bon réflexe est donc gradué : une règle deny pour les chemins connus, le bac à sable pour une garantie forte, et un hook quand la décision demande de la logique que les règles n’expriment pas, comme n’autoriser la lecture qu’à certaines heures, journaliser chaque tentative, ou vérifier le contenu d’une modification avant de l’accepter.
À vous de jouer
- Mettez en place la configuration et le script ci-dessus, puis déroulez les trois tests (lecture, modification, recherche) et le contre-test avec
cat. - Ajoutez la règle
denyet refaites le contre-test :cat .envest maintenant bloqué. - Étendez le script pour journaliser chaque tentative bloquée dans
.claude/blocked.logavec l’heure et le nom de l’outil (event.tool_name). - Commitez
.claude/settings.jsonet.claude/hooks/, clonez le dépôt dans un autre dossier et vérifiez que le hook fonctionne sans modification.
Sources officielles : Automate actions with hooks · Hooks reference · Configure permissions · Configure the sandboxed Bash tool
Points clés à retenir
- Forme exec (
command+args) et${CLAUDE_PROJECT_DIR}rendent un hook partageable, sans chemin absolu propre à une machine - Le script accumule stdin, extrait le chemin (
file_pathoupath), normalise les séparateurs, compare, et sort avec 2 et un message sur stderr pour bloquer - Un hook ne voit que les outils de son matcher :
cat .envpar Bash lui échappe - Pour protéger des chemins, une règle
denyRead(…)est plus complète ; le bac à sable donne la garantie système ; le hook sert quand il faut de la logique claude --debugjournalise chaque exécution de hook avec son code de sortie