Permissions, hooks et garde-fous
Les mécanismes qui contraignent réellement Claude Code : règles de permission, hooks de cycle de vie, isolation. Ce qui est appliqué par le client, quoi que le modèle décide.
À la fin de ce chapitre, vous saurez
- Écrire des règles de permission qui autorisent le travail sans ouvrir la porte
- Choisir le bon niveau de configuration selon ce qui doit être partagé
- Poser des hooks qui garantissent un comportement, quoi que Claude décide
- Isoler une session qui doit tourner sans supervision
Dans ce chapitre
Le chapitre précédent a posé la distinction : un CLAUDE.md exprime une intention, une permission impose une contrainte. Ce chapitre traite des mécanismes qui contraignent réellement.
C’est aussi le chapitre qui permet de travailler vite sans travailler dangereusement — les deux ne s’opposent que si l’on n’a pas configuré les garde-fous.
Les fichiers de configuration
Quatre niveaux, dans l’ordre de priorité décroissante.
| Niveau | Emplacement | Partagé | Usage |
|---|---|---|---|
| Politique gérée | Déployé par l’informatique (/etc/claude-code/managed-settings.json sur Linux) | Oui | Non contournable |
| Local | .claude/settings.local.json | Non, ignoré par git | Vos réglages sur ce projet |
| Projet | .claude/settings.json | Oui, versionné | Réglages de l’équipe |
| Utilisateur | ~/.claude/settings.json | Non | Vos réglages, tous projets |
La politique gérée l’emporte sur tout et ne se contourne pas.
Une exception importante : les règles de permission fusionnent entre les niveaux au lieu de s’écraser. Une interdiction posée au niveau projet reste active même si votre fichier local autorise largement.
/status affiche les fichiers effectivement chargés et les règles actives. Un fichier au JSON invalide n’apparaît pas — c’est la première chose à vérifier quand une règle semble ignorée.
Les règles de permission
Trois listes, une syntaxe.
{
"permissions": {
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint)",
"Bash(git status)",
"Bash(git diff *)",
"Read(./src/**)",
"Edit(./src/**)"
],
"ask": [
"Bash(git push *)",
"Bash(npm publish *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Edit(./migrations/**)",
"Bash(rm -rf *)",
"Bash(curl *)"
]
}
}Trois choses à retenir sur la syntaxe.
Le nom de l’outil, puis un motif entre parenthèses. Bash, Read, Edit, Write, et les outils apportés par les serveurs MCP sous la forme mcp__serveur__outil.
Les motifs acceptent * et **. Read(./src/**) couvre toute l’arborescence.
Le refus gagne toujours. C’est ce qui permet d’autoriser largement le travail courant tout en fermant quelques portes précises.
Les hooks
Un hook est une commande exécutée automatiquement à un point précis du cycle de vie, quelle que soit la décision du modèle. C’est la différence de nature avec une instruction.
Ils se déclarent dans les mêmes fichiers de réglages, avec une structure à trois niveaux : l’événement, un groupe de filtrage, les gestionnaires.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"",
"timeout": 30
}
]
}
]
}
}Les événements les plus utiles au quotidien :
| Événement | Quand | Usage typique |
|---|---|---|
SessionStart | Ouverture ou reprise de session | Injecter le contexte : branche, tickets en cours |
UserPromptSubmit | Avant traitement d’un message | Ajouter du contexte, refuser un message |
PreToolUse | Avant l’exécution d’un outil | Bloquer une action |
PostToolUse | Après le succès d’un outil | Formater, relancer les tests, journaliser |
Stop | Fin de la réponse | Vérification finale, notification |
PreCompact | Avant condensation du contexte | Sauvegarder ce qui ne doit pas se perdre |
Le mécanisme de blocage tient au code de sortie : un hook qui sort en 2 bloque l’action, et son stderr devient le message d’erreur transmis à Claude. Un code 0 laisse passer ; un JSON en sortie permet un contrôle plus fin.
Un hook qui bloque
L’exemple canonique : empêcher toute écriture hors du dépôt.
#!/usr/bin/env bash
# .claude/hooks/protege-hors-depot.sh
# Refuse toute écriture dont le chemin sort du répertoire du projet.
set -euo pipefail
entree=$(cat)
chemin=$(printf '%s' "$entree" | jq -r '.tool_input.file_path // empty')
[ -z "$chemin" ] && exit 0
racine=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
absolu=$(cd "$(dirname "$chemin")" 2>/dev/null && pwd -P)/$(basename "$chemin")
case "$absolu" in
"$racine"/*) exit 0 ;;
*)
echo "Écriture refusée hors du dépôt : $absolu" >&2
exit 2
;;
esac{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protege-hors-depot.sh" }
]
}
]
}
}Ce que ce hook garantit et qu’aucune instruction ne garantit : quelle que soit la façon dont Claude formule son action, elle est refusée.
Un hook qui referme la boucle
L’usage le plus rentable au quotidien : relancer la vérification après chaque modification, pour que Claude voie immédiatement ce qu’il a cassé.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npm run lint:changed --silent || true",
"timeout": 60,
"async": true
}
]
}
]
}
}Le async évite de bloquer la session sur une vérification longue.
Un hook qui injecte du contexte
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"Branche : $(git branch --show-current) — dernier commit : $(git log -1 --format=%s)\""
}
]
}
]
}
}La sortie standard d’un hook SessionStart est ajoutée au contexte. C’est une manière élégante de donner à Claude l’état courant sans l’écrire dans CLAUDE.md.
Isoler une session
Quand une session doit tourner sans supervision — traitement long, intégration continue, travail nocturne — la bonne réponse n’est pas d’élargir les permissions : c’est de réduire ce qui peut être atteint.
Trois niveaux, du plus léger au plus solide.
Un dépôt jetable. Un clone frais, sans secrets, sans accès aux environnements. Ce qui peut être cassé se jette.
Un worktree git dédié. git worktree add ../travail-auto isole les modifications de votre copie de travail sans dupliquer le dépôt.
Un conteneur. L’isolation réelle : système de fichiers, réseau et identifiants contrôlés. C’est le seul contexte où un mode sans confirmation se justifie.
Une combinaison qui marche bien : conteneur + permissions larges à l’intérieur + réseau restreint en sortie. Claude travaille librement dans une boîte dont les parois sont sûres.
Une configuration d’équipe qui tient
La situation
Une équipe de six personnes sur une application qui manipule des données clients. Il faut que Claude Code soit utilisable au quotidien sans que quiconque puisse, même par accident, exposer un secret ou toucher aux migrations.
Le fichier de projet, versionné
.claude/settings.json :
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(pnpm run *)",
"Bash(pnpm test *)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Read(./src/**)",
"Read(./tests/**)",
"Edit(./src/**)",
"Edit(./tests/**)"
],
"ask": [
"Bash(git push *)",
"Bash(gh pr create *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/credentials*)",
"Edit(./migrations/**)",
"Edit(./.github/workflows/**)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(psql *)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "pnpm exec prettier --write \"$CLAUDE_FILE_PATHS\"", "timeout": 30 }
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/refuse-prod.sh" }
]
}
]
}
}Le hook refuse-prod.sh refuse toute commande dont la ligne contient une chaîne de connexion de production, quelle que soit sa forme.
Le fichier local, personnel
Chacun ajoute dans .claude/settings.local.json ce qui lui est propre :
{
"permissions": {
"allow": ["Bash(docker compose *)"]
}
}Les autorisations s’ajoutent, les interdictions du projet restent actives — c’est la règle de fusion.
La vérification
Chacun lance /status et vérifie que les trois fichiers sont chargés. Puis quelqu’un demande explicitement à Claude de lire .env, pour confirmer que le refus fonctionne.
Ce que ça change
Le mode d’acceptation des éditions devient utilisable : Claude travaille vite, et les trois choses qui pouvaient mal tourner sont fermées par le client, pas par une phrase dans un fichier Markdown.
Ce qu’il faut retenir
Vérifier que c’est passé
3 questions. Rien n’est enregistré, rien n’est envoyé : c’est pour vous.
Le garde-fou qui compte chez vous
Identifiez, dans un projet réel, l’action que vous ne voulez sous aucun prétexte voir exécutée automatiquement : une commande de déploiement, une écriture dans un dossier de migrations, la lecture d’un fichier de secrets. Écrivez la règle deny correspondante dans .claude/settings.json, puis vérifiez avec /status qu’elle est bien chargée, et demandez à Claude de faire précisément cette chose pour confirmer qu’il est bloqué.
Un indice, si vous bloquez
Testez vraiment. Une règle mal écrite — un chemin relatif au lieu d’un glob, une majuscule oubliée sur le nom de l’outil — ne bloque rien et donne un faux sentiment de sécurité.