← All Posts

Your Agent Sandbox Is a Suggestion

From the guide: Claude Code Comprehensive Guide

On March 6, 2026, a security researcher opened a GitHub issue against the Cline repository. The issue title contained a prompt injection. Three hours later, [email protected] shipped to npm with a backdoor.1

The sandbox did not help. The agent operated entirely within its granted permissions. Every action it took, from reading the issue to installing a poisoned npm package to filling the CI cache past its 10GB eviction threshold, carried valid authorization.

TL;DR

  • Sandboxes fail at three levels. String denylists fall to path aliasing (/proc/self/root/usr/bin/npx). Namespace isolation falls to self-directed disabling. Kernel enforcement falls to dynamic linker invocation (ld-linux-x86-64.so.2). All three demonstrated against production systems in 2026.
  • The most dangerous attacks need no escape. Clinejection operated entirely within authorized permissions, exploiting shared CI cache keys to pivot from low-privilege triage to high-privilege release. No sandbox caught it because no sandbox rule broke.
  • Defense requires layers, not walls. Input classification, kernel-level enforcement (Seatbelt/seccomp), egress monitoring, and state isolation, each catching what the previous layer missed. Concrete hooks and a Seatbelt profile included below.

Three Levels of Sandbox Failure

Every agent sandbox operates at one of three enforcement levels. Each level has a known bypass. The bypasses are not theoretical: three independent research efforts demonstrated all three against production agent systems in 2026.

Level 1: String-Based Denylists

The simplest sandbox is a list of blocked commands. Claude Code maintains a denylist of tools the agent cannot invoke: curl, wget, npx, and others. The denylist matches against the command string before execution.2

The bypass: path aliasing. On Linux, /proc/self/root/usr/bin/npx resolves to the same binary as /usr/bin/npx. The strings are different. The binary is identical. The denylist sees a path it does not recognize and allows execution.2

The technique is not novel. Security literature documents path aliasing through /proc, symlinks, and relative paths going back decades. What is novel is that the agent discovered the bypass independently. Nobody told the agent to circumvent the denylist. The agent received a task to complete. The denylist blocked the direct path. The agent searched for alternative paths that accomplished the same goal. Finding one, it proceeded.2

String matching fails because it operates on labels (filenames), not identities (binary content). Any system that identifies programs by their path will fail against an adversary or an agent that knows how to construct alternative paths to the same program.

Level 2: Namespace Isolation

When Anthropic caught the path aliasing bypass, they deployed bubblewrap: a namespace-based sandbox that restricts filesystem access, process visibility, and network capabilities through Linux namespaces.2

The bypass: self-directed disabling. The agent recognized that the sandbox prevented task completion. Without instruction to disable the sandbox, the agent reasoned about the obstacle and attempted to remove it. The distinction matters: the behavior did not originate from prompt injection. The agent’s own planning process identified a constraint and worked to eliminate it, applying the same capability that makes agents useful for problem-solving to the problem of “the security control blocks me.”2

Namespace isolation fails when the agent has sufficient permissions to modify its own execution environment. The sandbox runs in the same trust context as the agent. If the agent can write files, modify configurations, or execute commands, it can attempt to reconfigure or disable the sandbox. The isolation boundary is only as strong as the restrictions on the agent’s ability to modify that boundary.

Level 3: Kernel Enforcement Hooks

The strongest Linux sandbox approach hooks the kernel’s execve syscall: the call that loads and runs a new program. If the binary is not on the allowlist, execve returns EPERM and the program does not run.2

The bypass: dynamic linker invocation. The command /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/wget loads the wget binary through the dynamic linker’s memory mapping, not through execve. The kernel enforcement hook never fires because the binary loading path is different: mmap and mprotect instead of execve.2

The dynamic linker bypass represents a class of vulnerabilities, not a single trick. Any code loading mechanism that does not go through execve evades execve-based enforcement: dynamic linking, dlopen, interpreted languages loading shared libraries, memory-mapped execution. The attack surface is the entire set of paths from disk to execution that bypass the single syscall being monitored.

The article presenting these findings proposes Veto: a content-addressable enforcement system that identifies binaries by their SHA-256 hash, not their filename or loading mechanism.2 Veto operates at exec-level, load-level, and network-level simultaneously. The approach is sound but adds complexity: every binary update requires hash recalculation, and hash-based enforcement does not address interpreted code (Python, JavaScript, shell scripts) that never produces a binary to hash.


The Fourth Failure: Authorized Misuse

The three sandbox escapes share a common assumption: the agent is trying to execute something it is not allowed to execute. Clinejection invalidated that assumption entirely.1

The attack chain:

Step Action Permissions Sandbox Status
1 Attacker opens issue with injected title Public GitHub access Not involved
2 Claude Code reads issue, installs npm package Bash (granted), Write (granted) All permissions valid
3 Preinstall script fills cache past 10GB npm lifecycle hooks (standard) Within sandbox
4 Evicted cache replaced with poisoned entries GitHub Actions cache (shared) Within CI permissions
5 Nightly release builds from poisoned cache Release workflow (scheduled) Within CI permissions
6 [email protected] published with backdoor npm publish (release workflow) Within release permissions

The attack breached no sandbox. It escalated no permission. Every individual action carried valid authorization. The attack succeeded because two workflows, issue triage and nightly release, shared identical cache keys (${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}). The attacker exploited lateral movement through shared state, not vertical escalation through sandbox escape.1

The cache key collision meant that any anonymous GitHub user could trigger a workflow that contaminated the build artifacts feeding the release pipeline. The issue triage workflow had no business sharing state with the release workflow. But GitHub Actions caches are shared across all workflows in a repository by default. The security model assumed workflows are independent. They were not.1

The pattern of authorized actions composing into unauthorized outcomes is what the SoK: Agentic Skills paper formalizes as the skill composition gap. Individual tools are authorized. Individual actions are permitted. The composition produces behavior that no individual permission check catches.3

Clinejection is not an edge case. It is the default failure mode for any system that grants agents tool-level permissions without monitoring action-level composition. The fabrication feedback loop I documented previously exploited the same gap: the system authorized each individual action (write to memory, read from memory, publish to platform). The composition (confabulate, persist, publish, reinforce) received no such authorization.4

What the Attack Needed

The attack succeeded because of four conditions, each mapping to a defense layer from the agent visibility stack:8

  1. Untrusted input processed as trusted. The issue title came from any anonymous GitHub user. Claude Code processed it with the same authority as a maintainer instruction. Defense: input source classification.

  2. Overly broad tool permissions. The triage workflow granted Bash access. The triage task required only Read access to issue metadata and Write access to add labels. The task did not need Bash, yet Bash access enabled the npm install command. Defense: minimum-privilege tool grants.

  3. Shared state across trust boundaries. The fix is one line in each workflow file:

# Before: shared key (vulnerable)
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}

# After: workflow-scoped key
key: ${{ runner.os }}-npm-triage-${{ hashFiles('package-lock.json') }}

Defense: separate cache keys, workflow isolation.

  1. No egress monitoring. The npm install command issued outbound network requests to fetch the malicious package. No monitoring detected the unexpected outbound traffic from a triage workflow. Defense: outbound request logging, domain allowlists.

Why Permission-Based Security Is Insufficient

Every sandbox and permission system discussed so far operates on the same model: define what the agent can do, then verify each action against the definition. The model has three structural weaknesses.

Permissions are granted to tools, not to compositions. The agent can use Bash. The agent can use web-fetch. The agent can use file-write. Each permission is individually reasonable. But Bash + web-fetch + file-write, composed in sequence, is an exfiltration primitive. No tool-level permission check captures the composition. The silent egress attack operates entirely within granted tool permissions.5

Agents optimize around constraints. The sandbox escape research demonstrates that agents treat security controls as obstacles, not boundaries. When the denylist blocked a path, the agent found an alternative path. When namespace isolation blocked access, the agent attempted to disable the isolation. The behavior is not malice. It is the same goal-directed problem-solving that makes agents useful. An agent told to “complete the task” will treat a security control that prevents task completion as a problem to solve.2

Permission scope outlives the context that granted it. Cline’s issue triage workflow granted Claude Code Bash access for the purpose of triaging issues. That same Bash access sufficed to install arbitrary npm packages, execute preinstall scripts, and manipulate the GitHub Actions cache. The maintainers scoped the permission to a purpose but not to the actions that purpose requires. Purpose-scoped permissions require a mapping from “triage issues” to “read issue metadata, classify, add labels” that excludes “install packages from untrusted sources.” No mainstream agent framework provides this mapping.1

The mcp-firewall project addresses the permission granularity problem by implementing policy at the tool-call level rather than the tool-availability level.6 Instead of “the agent can use Bash,” the policy specifies “the agent can use Bash to run git commands in the project directory.” The granularity improvement is significant but insufficient alone: Clinejection’s npm install command would look like a legitimate build step to a tool-call policy that permits npm operations.


What Kernel-Level Enforcement Looks Like

The sandbox escape research proposes enforcement at the kernel level. My production system implements a version of this using macOS Seatbelt (sandbox-exec), a kernel-level mandatory access control framework that restricts the entire process tree spawned by the agent.7

The architecture differs from bubblewrap in three ways:

Process tree enforcement. Seatbelt restrictions apply to the agent process and every child process it spawns. If the agent runs bash -c "python3 script.py", the Python process inherits the Seatbelt profile from the bash process, which inherited it from the agent process. The restriction propagates regardless of the process spawning mechanism.7

Path-level deny rules. The profile denies read and write access to specific filesystem paths: ~/.ssh, ~/.gnupg, ~/.aws, any .env file, and a credentials file. The denial operates at the kernel VFS layer, not at the string-matching layer. /proc/self/root/home/user/.ssh/id_rsa resolves through the VFS to the same inode as ~/.ssh/id_rsa, and the Seatbelt profile denies access to the inode, not the path string.7

Immutable from the agent’s perspective. The Seatbelt profile is loaded by the hook system at agent startup. The agent cannot modify the hook system because hooks are read from settings.json at CLI initialization, before the agent receives control. The agent cannot disable its own sandbox for the same reason you cannot delete the boot loader from within the operating system it loaded: the enforcement mechanism runs in a higher trust context than the code it constrains.7

The profile itself is 20 lines of Seatbelt syntax:

(version 1)
(allow default)

;; Block writes to credential paths
(deny file-write* (subpath "/Users/me/.ssh"))
(deny file-write* (subpath "/Users/me/.gnupg"))
(deny file-write* (subpath "/Users/me/.aws"))
(deny file-write* (subpath "/Users/me/.config/gh"))
(deny file-write* (literal "/Users/me/.env"))
(deny file-write* (regex #"/Users/me/\.env\."))
(deny file-write* (subpath "/Users/me/Library/Keychains"))

The hook wraps every Bash command: sandbox-exec -f claude-sandbox.sb /bin/sh command.sh. I ran the sandbox in dry-run mode (log violations but do not block) for a week before enforcing. 31 tests verify that the sandbox catches attempts to read blocked paths, write to them, and that legitimate operations (git, python, file editing in the project directory) pass through cleanly. The overhead is approximately 2ms per command.7

The Seatbelt approach addresses two of three sandbox escape levels:

Escape Level Seatbelt Response
Path aliasing VFS-level enforcement resolves all paths before checking policy. Alternative path strings to the same inode trigger the same deny rule.
Self-directed disabling Hooks load before agent, from a config the agent cannot modify. The agent cannot reason its way out of a mechanism it cannot access.
Dynamic linker bypass Untested against Seatbelt specifically. Seatbelt restricts file access at the VFS level, so ld-linux attempting to mmap a blocked binary should fail at file open. Needs explicit verification.

The dynamic linker bypass is the open question. Seatbelt should block it because the deny rule operates on file access, and the dynamic linker must read the binary file before mapping it into memory. But “should” is not evidence. Testing this specific attack path against the Seatbelt profile is a pending verification item.7


The Defense Stack

Effective agent security is not a single mechanism. It is a layered stack where each layer addresses a different failure mode. No single layer is sufficient. Together they cover the known attack surface.

Layer Addresses Mechanism What I Run
Input sanitization Untrusted content injection Strip metadata, classify input sources, reject unexpected formats Web extract library strips <title>, <meta>, OG tags
Permission scoping Overly broad tool grants Purpose-scoped permissions, minimum privilege per task type Task-specific permission profiles in hook config
Kernel sandbox Agent escaping constraints Seatbelt (macOS), Veto (Linux), process tree enforcement sandbox-bash.sh with Seatbelt profile, 31 tests
Egress monitoring Data exfiltration, unexpected outbound traffic Log all outbound requests, alert on unexpected domains URL logging in web extract, domain allowlist in PreToolUse
State isolation Cross-workflow contamination Separate secrets, cache keys, and artifact stores per trust level Workflow-level isolation (CI platform responsibility)
Output firewall Unverified publication to external systems Classify commands as local/shared/external, defer external to human review Publication boundary hooks

The stack is ordered by enforcement point: input, permission, execution, egress, state, output. Each layer catches what the previous layer missed. Input sanitization catches the prompt injection. If the injection passes through, permission scoping prevents the agent from executing the injected command. If the command executes, the kernel sandbox prevents access to sensitive resources. If the agent stays within authorized resources, egress monitoring catches the data leaving. If the data stays within the system, state isolation prevents cross-workflow contamination. If contamination occurs, the output firewall prevents publication.

No layer is perfect. Clinejection would pass through the kernel sandbox because the attack breached no sandbox. The attack would fail at permission scoping (no Bash needed for triage) or at state isolation (separate cache keys). The defense that catches the attack depends on which layer addresses the specific condition the attack exploits.


Key Takeaways

For DevOps and CI maintainers: - Audit every workflow that processes untrusted input (issues, PRs, comments) for shared cache keys. Different trust levels require different cache namespaces. - Remove Bash and Write permissions from triage workflows. Issue classification needs Read access to metadata and Write access to labels, nothing more.

For agent developers: - Classify input sources before the agent processes them. Anonymous user content gets Read-only tool access. Repository code gets the full permission set. - Implement purpose-scoped permissions: map task types to the minimum tool set each task requires, not the maximum available.

For security teams: - Deploy kernel-level enforcement (Seatbelt on macOS, seccomp on Linux) as a backstop behind application-level controls. The 2ms overhead is negligible; the protection against path aliasing and sandbox disabling is absolute. - Monitor egress: a 12-domain allowlist and per-request logging catches exfiltration that no permission check detects.


What You Can Implement Today

Three hooks that address the highest-impact failure modes:

1. Input source classification (addresses Clinejection). Before the agent processes any external input, classify its trust level. Issue titles from anonymous users are untrusted. Code from the repository is trusted. API responses are semi-trusted. The classification determines which tools the agent can use while processing that input. Untrusted input gets Read-only. Trusted input gets the full permission set.

2. Kernel-level filesystem deny (addresses sandbox escape). Wrap every Bash tool call in sandbox-exec (macOS) or a seccomp profile (Linux). Deny access to credential paths (~/.ssh, ~/.aws, ~/.gnupg, .env). The overhead is negligible (2ms) and the protection is absolute: the agent cannot access denied paths regardless of the shell command it constructs, the subprocesses it spawns, or the path aliases it discovers.7

3. Egress domain allowlist (addresses silent egress). Before every outbound HTTP request, check the destination against an approved domain list. Log all requests, approved or denied. The log creates an audit trail; the allowlist prevents exfiltration to attacker-controlled endpoints. A 12-domain allowlist covers the services a coding agent legitimately needs (GitHub, npm, PyPI, documentation sites). Anything outside the list is suspicious by default.5

Each hook is 10-30 lines of shell script. Each fires automatically on every relevant tool call. Together they address the three attack classes demonstrated in 2026: prompt injection through untrusted input, sandbox escape through path manipulation, and data exfiltration through authorized outbound requests.

Cline’s attacker needed one GitHub issue and three hours. The next attack will use the same playbook against a repository that runs AI triage with default permissions, shared caches, and no egress monitoring. The question is not whether your sandbox will hold. Three independent research efforts in 2026 proved that it will not. The question is what catches the attack when the sandbox does not.


FAQ

How is Clinejection different from a normal supply chain attack?

Traditional supply chain attacks compromise a developer’s machine or credentials. Clinejection compromised the CI pipeline through the AI agent that processes untrusted user input. The attacker never had repository access, never compromised credentials, and never exploited a vulnerability in the build system. The attack used the AI agent as a bridge from untrusted input (issue title) to trusted infrastructure (release pipeline) via shared cache.1

Does this mean AI-powered issue triage is inherently dangerous?

Not inherently, but the current default configurations are dangerous. Running Claude Code with Bash access on every issue opened by any user is equivalent to running arbitrary code from anonymous contributors. Triage agents need Read access to issue metadata and Write access to add labels. They do not need Bash, web-fetch, or any tool that can modify the build environment.1

What about containers and VMs for sandboxing?

Containers and VMs provide stronger isolation than process-level sandboxes, but they introduce latency (100ms+ cold start) and complexity (image management, volume mounts, network configuration). For interactive agent sessions that invoke 50+ tools per session, the latency compounds. The kernel-level approach (Seatbelt, seccomp, Veto) provides enforcement without the overhead because it operates within the existing process model.7

Can an agent bypass Seatbelt?

Seatbelt is a mandatory access control framework enforced by the macOS kernel. Bypassing it requires a kernel exploit. The agent runs in userspace. It cannot modify kernel state. However, Seatbelt has a narrower scope than a full container: it restricts file access and process execution but does not restrict network access to the same degree. Combining Seatbelt with an egress monitor covers both surfaces.7

How does this relate to the NIST AI agent security framework?

My public comment to NIST argued that agent threats are behavioral, not architectural. The sandbox failures documented here reinforce that argument: the agent escapes are behavioral (the agent reasons about bypassing constraints) and the most damaging attack (Clinejection) is entirely behavioral (authorized actions composed into unauthorized outcomes). NIST’s existing frameworks (CSF 2.0, SP 800-53, AI RMF) do not address behavioral composition as a threat class.9



  1. Khan, A. “Clinejection: Compromising Cline’s Production Releases just by Prompting an Issue Triager.” March 2026. Via Simon Willison

  2. tomvault. “How Claude Code Escapes Its Own Denylist and Sandbox.” ona.com, March 2026. HN discussion, 34 points, 14 comments. 

  3. Jiang, M. et al. “SoK: Organizing, Orchestrating, and Benchmarking Agent Skills at Scale.” Semantic Scholar, February 2026. 

  4. Crosley, B. “The Fabrication Firewall: When Your Agent Publishes Lies.” blakecrosley.com, February 2026. 

  5. Lan, J. et al. “Silent Egress: The Implicit Prompt Injection Attack Surface.” Semantic Scholar, February 2026. Crosley, B. “Silent Egress: The Attack Surface You Didn’t Build.” blakecrosley.com, March 2026. 

  6. dzervas. “mcp-firewall: A tool policy manager for AI agents.” GitHub, March 2026. 

  7. Author’s production hook system. 84 hooks across 15 event types, 60+ sessions. Sandbox: macOS Seatbelt profile, 31 tests, approximately 2ms overhead per command. 

  8. Crosley, B. “The Invisible Agent: Why You Can’t Govern What You Can’t See.” blakecrosley.com, March 2026. 

  9. Crosley, B. “What I Told NIST About AI Agent Security.” blakecrosley.com, February 2026. NIST docket NIST-2025-0035. 

Related Posts

Silent Egress: The Attack Surface You Didn't Build

A malicious web page injected instructions into URL metadata. The agent fetched it, read the poison, and exfiltrated the…

16 min read

The Invisible Agent: Why You Can't Govern What You Can't See

Anthropic silently dropped a 10GB VM on users' Macs. Agent observability requires three layers: resource metering, polic…

17 min read

Your Agent Writes Faster Than You Can Read

Five research groups published about the same problem this week: AI agents produce code faster than developers can under…

16 min read