Aller au contenu
Développement medium

Sécuriser son settings.json Claude Code : permissions, secrets et dérive

23 min de lecture

Logo Claude Code - rules ciblées par dossier

Un settings.json Claude Code ne se dégrade pas d'un coup. Il grossit d'une règle à la fois, à chaque fois qu'on répond « ne plus me demander » à une question qui paraissait anodine. Au bout de quelques mois, le fichier pèse plusieurs centaines de kilo-octets, contient des milliers de règles que personne ne relit, et parfois un mot de passe en clair au milieu d'une commande approuvée six semaines plus tôt.

Ce n'est pas un défaut de discipline, c'est une conséquence du fonctionnement. La documentation officielle est explicite : « Permission rules behave differently because they merge across scopes rather than override ». Les règles de permission fusionnent entre les portées au lieu de s'écraser. Rien ne remplace, rien n'expire, rien ne nettoie. Le fichier ne peut que croître.

Ce guide part de ce mécanisme, montre ce que chaque réponse du dialogue de permission écrit réellement, comment un secret y entre sans qu'on le remarque, ce que deny protège vraiment, et comment auditer puis reconstruire une configuration tenable.

  • Le dialogue de permission : ce que « ne plus demander » écrit, et où
  • Pourquoi les règles s'accumulent : la fusion entre portées
  • Comment un secret entre dans la configuration sans qu'on le voie
  • Ce que deny protège, et ce qu'il ne protège pas
  • Auditer : les outils existants, ce qu'ils trouvent et ce qu'ils ratent
  • Reconstruire une base saine et éviter la rechute
  • Claude Code installé et utilisé sur au moins un projet (installation).
  • La structure des fichiers de configuration comprise : portées, syntaxe des règles, différence entre fichier partagé et fichier local. Elle est traitée dans settings.json avancé et permissions par projet, que ce guide complète du côté sécurité.

Le dialogue de permission : ce que chaque réponse écrit

Section intitulée « Le dialogue de permission : ce que chaque réponse écrit »

Tout commence là. Quand Claude Code demande l'autorisation d'exécuter une commande, les réponses n'ont ni la même portée ni la même durée de vie, et cette différence n'est pas affichée à l'écran.

Type d'outilApprobation demandéeEffet de « ne plus demander »
Lecture seuleNon, dans les répertoires de travailSans objet
Commandes BashOui, sauf un jeu de commandes en lecture seulePermanent, par dépôt et par commande
Modification de fichierOuiJusqu'à la fin de la session

La ligne du milieu est celle qui remplit les fichiers. Une approbation permanente est écrite sur le disque, dans .claude/settings.local.json à la racine du dépôt git. Une approbation de modification de fichier, elle, n'écrit rien : elle disparaît quand la session se termine.

Autrement dit, la case qu'on coche machinalement dix fois par jour sur des commandes shell est la seule qui laisse une trace définitive sur le disque.

Le point le plus contre-intuitif. Approuver définitivement une commande enchaînée ne produit pas une règle, mais une par sous-commande : « When you approve a compound command with "Yes, don't ask again", Claude Code saves a separate rule for each subcommand that requires approval, rather than a single rule for the full compound string ». La documentation précise la borne : jusqu'à cinq règles pour une seule approbation.

Un cd projet && npm install && npm test approuvé une fois peut donc écrire une règle Read pour le répertoire, une pour npm install, une pour npm test. En une frappe, trois lignes de plus dans le fichier.

Les réglages ordinaires suivent une précédence classique : managed l'emporte sur les arguments de ligne de commande, qui l'emportent sur local, puis project, puis user. Le plus prioritaire gagne, les autres sont ignorés.

Les permissions échappent à cette règle. Elles fusionnent. Une règle écrite dans le fichier utilisateur ne sera jamais remplacée par une règle de projet : les deux coexistent et s'additionnent. Multipliez par le nombre de dépôts et par les mois d'usage, et l'ordre de grandeur devient celui-ci, mesuré sur une installation réelle :

Fenêtre de terminal
python3 -c "import json,sys; d=json.load(open('$HOME/.claude/settings.json')); \
print(len(d.get('permissions',{}).get('allow',[])), 'regles allow')"
1917 regles allow

Pour 219 Ko de fichier et 291 préfixes de commandes distincts. Trois cents intentions réelles, exprimées en presque deux mille lignes, parce que chaque variante d'une même commande a produit sa propre entrée : kubectl get pods -n argocd et kubectl get pods -n argocd -o wide sont deux règles différentes.

Personne ne relit deux mille règles. C'est précisément ce qui rend le fichier dangereux : il devient un endroit où des choses s'accumulent sans regard, y compris ce qui n'aurait jamais dû y entrer.

Voici le scénario, et il n'a rien d'exotique. Vous lancez une commande qui porte une variable d'environnement en préfixe, comme on le fait tous les jours avec un outil de migration ou un client de base de données :

Fenêtre de terminal
DATABASE_URL=postgresql://app:motdepasse@localhost:5432/app npx prisma migrate dev

Claude Code demande l'autorisation. Vous répondez « ne plus demander », parce que vous allez rejouer cette migration dix fois. La règle enregistrée contient la ligne de commande complète, mot de passe compris. Elle est maintenant dans un fichier JSON, en clair, et elle y restera.

Le même mécanisme attrape les jetons passés en variable, les en-têtes Authorization d'un curl, les clés d'API en argument. Sur l'installation auditée pour ce guide, deux secrets ont été retrouvés de cette façon : une URL PostgreSQL avec son mot de passe, et un secret client OIDC.

La parade est simple et tient à l'habitude : sortir le secret de la ligne de commande avant de lancer quoi que ce soit. Un fichier .env chargé par l'outil, une variable exportée dans le shell, ou un gestionnaire de secrets. La commande approuvée ne contient alors qu'un nom de variable.

Empêcher le secret d'entrer plutôt que le retrouver après

Section intitulée « Empêcher le secret d'entrer plutôt que le retrouver après »

Auditer, c'est constater. Le fichier contient déjà le secret, il est déjà sur le disque, il est déjà dans vos sauvegardes. La seule mesure qui protège vraiment consiste à refuser la commande avant qu'elle soit approuvée.

Claude Code le permet avec un hook PreToolUse sur Bash. Le hook reçoit la commande en JSON sur son entrée standard, et peut répondre deny avec une explication qui s'affiche à l'écran.

C'est le point à connaître avant d'écrire le hook. Voici ce que gitleaks 8.30.0 rend sur des lignes de commande, mesuré :

Commande testéegitleaks
GITHUB_TOKEN=ghp_…bloque
STRIPE_KEY=sk_live_…bloque
CLIENT_SECRET=8f3d9a2b…bloque
Authorization: Bearer eyJ…bloque
DATABASE_URL=postgresql://app:motdepasse@db:5432/applaisse passer
MYAPP_PASSWORD=hunter2ButLongerlaisse passer

gitleaks excelle sur les formats reconnaissables, ceux des fournisseurs. Il laisse passer le mot de passe générique, qui n'a aucune forme particulière. Or c'est précisément celui qu'on retrouve dans les settings.json : une URL de base de données de développement, un identifiant de lab.

La parade tient en une expression régulière, qui attrape exactement ce que gitleaks rate :

Fenêtre de terminal
grep -qE '://[^/ :]+:[^@/ ]{3,}@'

Trois détecteurs en cascade, du moins cher au plus cher : l'URL avec identifiants, la variable sensible affectée en clair, puis gitleaks pour tout le reste.

~/.claude/hooks/bloque-secrets.sh
#!/usr/bin/env bash
set -uo pipefail
commande=$(cat | jq -r '.tool_input.command // empty')
[ -z "$commande" ] && exit 0
motif=""
if printf '%s' "$commande" | grep -qE '://[^/ :]+:[^@/ ]{3,}@'; then
motif="un mot de passe dans une URL"
elif printf '%s' "$commande" \
| grep -qE '\b[A-Z_]*(TOKEN|SECRET|PASSWORD|APIKEY|API_KEY)[A-Z_]*=[^ ]{6,}'; then
motif="une variable sensible affectee en clair"
fi
# gitleaks coute environ 300 ms au demarrage : on ne le lance que si la
# commande porte au moins un indice de secret.
#
# La garde `command -v` n'est PAS facultative : sans elle, un gitleaks absent
# rend « command not found », donc un code non nul, et le `||` conclurait a un
# secret sur TOUTE commande contenant un `=`.
INDICES='=|://|-----BEGIN|[Aa]uthorization|[Bb]earer|[Tt]oken|[Ss]ecret|[Kk]ey|[Pp]ass'
if [ -z "$motif" ] && command -v gitleaks >/dev/null 2>&1 \
&& printf '%s' "$commande" | grep -qE "$INDICES"; then
printf '%s' "$commande" | gitleaks stdin --no-banner --redact >/dev/null 2>&1 \
|| motif="un secret reconnu par gitleaks"
fi
[ -z "$motif" ] && exit 0
jq -n --arg r "Commande refusee : elle contient $motif. L'approuver en « ne plus
demander » ecrirait le secret en clair dans settings.local.json. Sortez-le de la
ligne de commande, puis relancez." '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $r
}
}'

La déclaration se fait dans ~/.claude/settings.json :

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/bloque-secrets.sh" }
]
}
]
}
}

Le pré-filtre n'est pas une coquetterie : le hook s'exécute avant chaque commande Bash. Sans lui, chaque git status payait le démarrage de gitleaks.

VersionTemps par commande
Sans pré-filtre344 ms
Avec pré-filtre8 ms

Sur huit cas de test, les quatre commandes portant un secret sont refusées et les quatre commandes légitimes passent, y compris NODE_ENV=production npm run build, DATABASE_URL=$(cat .env.local) npx prisma migrate et psql "$DATABASE_URL". Ces trois dernières sont exactement la bonne pratique : le secret n'est plus dans la ligne de commande, seulement son nom.

deny est la seule liste qui bloque. Elle mérite d'être écrite à la main, et soigneusement, parce que deux détails de syntaxe changent tout.

Une règle comme Bash(bash) ne bloque que la commande nue. Elle laisse passer bash script.sh, qui est pourtant l'usage réel. Pour couvrir un préfixe, la syntaxe est Bash(bash:*).

{
"permissions": {
"deny": [
"Bash(bash:*)",
"Bash(sudo:*)",
"Read(//**/.env)",
"Read(~/.ssh/**)",
"Read(~/.aws/credentials)"
]
}
}

Les liens symboliques ne suivent pas la même règle

Section intitulée « Les liens symboliques ne suivent pas la même règle »

Le traitement est volontairement asymétrique, et c'est une bonne conception : une règle allow ne s'applique que si le lien et sa cible correspondent tous les deux, alors qu'une règle deny s'applique dès que l'un des deux correspond. Un lien symbolique placé dans un répertoire autorisé mais pointant à l'extérieur déclenche donc une demande, il ne passe pas en silence.

FichierPortéeVersionné
~/.claude/settings.jsonToutes vos sessions, tous vos projetsNon
.claude/settings.jsonLe dépôt, pour toute l'équipeOui
.claude/settings.local.jsonLe dépôt, pour vous seulNon, gitignoré automatiquement

Claude Code configure git pour ignorer le fichier local qu'il crée lui-même. Un fichier local créé à la main n'hérite pas de cette protection, et c'est par là que des règles personnelles finissent commitées.

Le piège moins connu concerne la confiance accordée à l'espace de travail. La documentation note que les règles allow du fichier local prennent effet « sans l'étape de confiance qu'exigent celles de .claude/settings.json », mais cette formulation demande deux nuances importantes.

D'abord, elle ne vaut plus telle quelle : depuis la version 2.1.207, dans un dossier que vous n'avez pas encore approuvé, les règles allow du fichier local passent par le même contrôle de confiance que celles du projet, jusqu'à acceptation du dialogue. Deux exceptions subsistent, hors dépôt git et dans votre propre répertoire de configuration.

Ensuite, si le dépôt fournit lui-même le fichier, par exemple en le commitant, la confiance d'espace de travail s'applique de nouveau. C'est précisément le scénario des vulnérabilités traitées dans Sécuriser Claude Code.

En entreprise, la réponse propre existe : allowManagedPermissionRulesOnly, disponible uniquement en réglages administrés, neutralise toutes les règles allow, ask et deny définies au niveau utilisateur et projet. Seules les règles administrées s'appliquent.

AgentShield est un auditeur généraliste qui fonctionne sous Linux. Il découvre ~/.claude/ seul et note la configuration.

Fenêtre de terminal
npx ecc-agentshield scan
Grade: C (70/100)
Score Breakdown
Secrets ██████████░░░░░░░░░░ 50
Permissions ░░░░░░░░░░░░░░░░░░░░ 0
Hooks ████████████████████ 100
MCP Servers ████████████████████ 100

Il est solide sur les permissions trop larges, les hooks et les serveurs MCP, dont il vérifie les noms contre des typosquats connus. Son mode d'analyse par modèle est optionnel : sans ANTHROPIC_API_KEY, il refuse de démarrer, donc un scan ordinaire n'envoie pas votre configuration à une API.

Trois commandes couvrent l'essentiel de ce que l'outil ne voit pas.

Chercher les secrets, en commençant par le motif qui rapporte le plus, une paire identifiant et mot de passe dans une URL :

Fenêtre de terminal
grep -nE '://[^/ :]+:[^@/ ]{3,}@' ~/.claude/settings.json

Compter la dérive, pour la surveiller dans le temps plutôt que la découvrir :

Fenêtre de terminal
python3 -c "import json; d=json.load(open('$HOME/.claude/settings.json')); \
p=d.get('permissions',{}); print({k: len(v) for k,v in p.items()})"
{'allow': 1917, 'deny': 10, 'ask': 25, 'additionalDirectories': 20}

Vérifier que les répertoires additionnels existent encore, car un chemin mal capturé y reste indéfiniment :

Fenêtre de terminal
python3 -c "
import json, os
d = json.load(open(os.path.expanduser('~/.claude/settings.json')))
for x in d.get('permissions', {}).get('additionalDirectories', []):
if not os.path.isdir(os.path.expanduser(x)):
print('inexistant ou malforme :', x)
"
inexistant ou malforme : /home/bob/Projets/test-astro-5/^\s*
inexistant ou malforme : /home/bob/Projets/lab-ansible/labs/000-prepare/{challenge

Ces deux entrées sont des fragments d'expression régulière et d'accolade capturés comme des chemins. Ils ne servent à rien, mais ils montrent que personne n'a relu le fichier depuis longtemps.

La bonne stratégie n'est ni de tout garder ni de tout interdire, mais de remplacer des milliers de règles littérales par quelques dizaines de règles de préfixe. Un fichier de trente règles se relit en une minute, donc il se relit vraiment, et c'est la seule propriété qui compte sur le long terme.

  1. Sauvegarder l'existant, pour pouvoir y repêcher une règle oubliée.

    Fenêtre de terminal
    cp ~/.claude/settings.json ~/.claude/settings.json.$(date +%F)
  2. Extraire les intentions réelles plutôt que de trier à la main. Le regroupement par préfixe montre les commandes qui reviennent vraiment.

    Fenêtre de terminal
    python3 -c "
    import json, os, re
    from collections import Counter
    d = json.load(open(os.path.expanduser('~/.claude/settings.json')))
    c = Counter()
    for r in d['permissions']['allow']:
    m = re.match(r'^Bash\(([\w.-]+(?: [\w.-]+)?)', r)
    if m: c[m.group(1)] += 1
    for k, n in c.most_common(25): print(f'{n:5d} {k}')
    "
  3. Réécrire en règles de préfixe, avec la syntaxe :* qui n'est reconnue qu'en fin de motif.

    {
    "permissions": {
    "allow": [
    "Bash(git status:*)",
    "Bash(git diff:*)",
    "Bash(kubectl get:*)",
    "Bash(npm run:*)"
    ],
    "ask": ["Bash(git push:*)"],
    "deny": ["Bash(sudo:*)", "Read(//**/.env)", "Read(~/.ssh/**)"]
    }
    }
  4. Vérifier que la configuration active est bien celle attendue, avec /status dans Claude Code, puis relancer un scan.

L'hygiène qui évite la rechute tient en trois réflexes. Réserver « ne plus demander » aux commandes de lecture dont la forme est stable. Sortir les secrets de la ligne de commande avant de lancer la commande, pas après. Et relire le fichier quand il dépasse une taille où on ne le relit plus, disons une centaine de règles.

Les symptômes ci-dessous se ressemblent, mais les causes sont indépendantes : un problème de portée (la règle n'est pas là où vous croyez), un problème de syntaxe (la règle ne correspond pas à ce que vous croyez), ou un problème d'accumulation. La colonne du milieu donne la vérification qui tranche.

SymptômeComment trancherSolution
Une règle deny ne bloque rienElle est en correspondance exacte, sans :*Passer en Bash(commande:*)
Le fichier grossit sans qu'on approuveLes commandes composées écrivent jusqu'à 5 règlesApprouver les commandes une par une
Une permission accordée ne survit pasC'était une modification de fichierNormal, ces approbations durent la session
Une règle s'applique sans être dans le fichier projetElle vient du fichier utilisateurLes permissions fusionnent, vérifier les trois portées
Un secret apparaît dans le fichiergrep -nE '://[^/ :]+:[^@/ ]{3,}@'Révoquer le secret, puis nettoyer
Un répertoire additionnel inconnuVérifier son existence sur le disqueFragment mal capturé, à supprimer
Le scan signale une clé qui n'en est pasLire la ligne pointéeFaux positif fréquent sur les chaînes de test
  1. Les permissions fusionnent entre portées, elles ne s'écrasent pas : le fichier ne peut que grossir.
  2. « Ne plus demander » sur une commande Bash écrit une règle permanente dans .claude/settings.local.json du dépôt.
  3. Une commande composée écrit jusqu'à cinq règles d'un seul coup.
  4. Une approbation de modification de fichier ne dure que la session et n'écrit rien.
  5. Un secret passé en préfixe de commande finit en clair dans la configuration, et doit être considéré comme compromis.
  6. deny en correspondance exacte ne couvre presque rien : la forme utile est Bash(commande:*).
  7. Le fichier local suit le contrôle de confiance comme le projet depuis la version 2.1.207, avec deux exceptions.
  8. Un auditeur générique rate des secrets réels : gardez une vérification propre, une expression régulière suffit.

Les questions ci-dessous portent sur les points où le comportement observé surprend : une règle qui ne bloque pas, un fichier qui grossit tout seul, une permission qui disparaît. Chaque réponse donne la commande qui permet de vérifier sur votre propre installation.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn