Skip to main content

Running agent code is becoming a sandboxing problem

Prompt injection will not be patched away. A hijacked coding agent can reach only what its sandbox, egress rules and credentials allow.

Updated 34 min readMarkdown
Running agent code is becoming a sandboxing problem

AI coding tools spent their first years producing text.

They suggested a line, then a function, then a file. A developer read the suggestion and decided whether it ran. Whatever the model wrote, a person stood between it and the machine.

That person is leaving the loop.

Claude Code, Codex, Cursor, Gemini CLI and GitHub Copilot now run shell commands themselves. They install packages, run tests, rewrite config and push branches, some of them unattended in CI or in a vendor's cloud. Anthropic's engineering team names what happens when a human approves every step: "approval fatigue".

The inputs did not get safer. An agent reads issues, READMEs, web pages, install scripts and MCP tool output, and any of it can carry instructions the model cannot reliably tell apart from yours. AI agents need boring engineering covers those basics. This post starts where that one stops.

The model will be tricked sometimes.

That is not a bug to file. It is a design input.

What matters is what the process can touch when it happens: which files, which hosts, which credentials, for how long. That makes running agent code a sandboxing problem, and most of the sandboxes shipping today are off by default or leave the network and credentials partly open.

The incident record is about reach, not jailbreaks

Since late 2024, and faster since mid-2025, coding agents have produced a steady record of public incidents. Read together, they share a shape.

None depended on a classic jailbreak. In most, attacker text arrived through an ordinary channel, the agent acted on it, and the outcome was decided by what the agent could already reach. In the MCPoison and Codex config rows no model decided anything: the product ran commands a repository declared.

Ten incidents, one pattern

Incident Date Input vector What the agent could reach Fix
GitHub MCP "toxic agent flow" (Invariant Labs) May 2025 Malicious issue in a public repo Private repos via the same token; a public PR as the exit No code fix; one repo per session, least privilege
Gemini CLI allowlist bypass (Tracebit) July 2025 Instructions hidden in a README.md Shell via an allowed grep prefix, plus network v0.1.14
Amazon Q Developer for VS Code, CVE-2025-8217 July 2025 Code merged via an over-scoped GitHub token in CodeBuild config A prompt to wipe local files and cloud resources (press reports); never ran, syntax error 1.85.0
Cursor CurXecute, CVE-2025-54135 (GHSA-4cxx-hrm3-49rm) August 2025 Injection in content read through MCP Writes to mcp.json, which launches commands Cursor 1.3.9
Cursor MCPoison, CVE-2025-54136 August 2025 MCP config swapped after approval Commands on reopen, no re-approval Cursor 1.3
Copilot in VS Code, CVE-2025-53773 August 2025 Injection in project content .vscode/settings.json, then the shell August 2025 Patch Tuesday
Nx "s1ngularity" August 2025 Malicious postinstall in compromised npm releases Local AI CLIs, files, the victim's GitHub account Versions removed by npm 26 to 27 August
Codex CLI project config, CVE-2025-61260 Fixed Aug 2025, published Dec 2025 Repo .env sets CODEX_HOME; project config declares MCP commands Commands at startup, no approval 0.23.0
PromptPwnd (Aikido Security) December 2025 Issue and PR text fed to CI agents Privileged CI tokens Per project
Clinejection (Adnan Khan) February 2026 Issue title in a triage prompt Bash in CI, the Actions cache, release tokens Workflow fixed after disclosure

The vectors are mundane: an issue, a README, a config file, an install script. So are the exits: a settings file, a shell, a token, an outbound request.

Every exit was a capability the agent already held

Read the reach column.

Johann Rehberger's Copilot finding is the clearest. Injected instructions got Copilot to write "chat.tools.autoApprove": true into .vscode/settings.json, turning off confirmations, then run shell commands. "The edits are immediately persistent, they are not in-memory as a diff to review." The agent could edit the file that governed its own permissions.

Cursor's August 2025 bugs follow the same line. MCPoison showed that Cursor approved an MCP server by key name, so a committed .cursor/rules/mcp.json could swap the command later. CurXecute, titled "Arbitrary code execution from Cursor Agent through a prompt injection via MCP Special Files", let injected content add an mcp.json entry, which according to Tenable's summary of the Aim Labs research ran without approval.

Check Point's Codex CLI finding needed no injection at all. A repository's .env set CODEX_HOME=./.codex, and the repository's own config.toml declared MCP commands that ran at startup with "no interactive approval".

Tracebit's Gemini CLI finding shows why command allowlists are weak. Allowing grep allowed anything that began with grep, because "Only very cursory validation logic is performed when comparing a shell input to the command whitelist". Whitespace padding hid the rest from the UI, and the CLI ran in its default no-sandbox mode.

Invariant Labs' GitHub MCP demo needed no bug. A malicious public issue steered an agent holding a token for private repositories into leaking them through a public pull request. Invariant says "this is not a flaw in the GitHub MCP server code itself" and "even state-of-the-art aligned models are vulnerable". The demo used Claude 4 Opus in Claude Desktop, and Invariant notes that many users switch tool confirmation to "Always Allow". The flaw was one token spanning two trust levels in one session.

Two rows need a qualifier

Amazon Q was a compromise of the agent product, not an injection into a running agent. Per AWS-2025-015, the code "was unsuccessful in executing due to a syntax error", which "prevented the malicious code from making changes to any services or customer environments". The Register reported that it was a prompt telling Q to wipe local files and cloud resources. Nothing was wiped.

The Nx advisory documents a workflow injection that led to npm token theft and malicious releases on 26 August 2025. The postinstall telemetry.js told a local agent to act as a "file-search agent" and "write it to /tmp/inventory.txt", and stolen data went to s1ngularity-repository repos in victims' own GitHub accounts. The advisory does not name the agents. Socket reported that the script looked for the Claude, Gemini and Amazon Q CLIs, and Snyk lists the flags it passed to switch off their prompts: --dangerously-skip-permissions, --yolo and --trust-all-tools.

Malware no longer needs its own file scanner when the victim has an agent and a flag that removes its prompts. That is the shift described in AI changed the software supply chain, seen from the attacker's side.

Anthropic's November 2025 report of a state-sponsored group operating Claude Code is a different problem: misuse by the operator, not hijacking. It is out of scope here.

Prompt injection will not be patched away

If injection were like SQL injection, the answer would be a fix and a regression test.

The people closest to it say it is not.

Security agencies and researchers agree on the diagnosis

The UK NCSC's post Prompt injection is not SQL injection (it may be worse) (December 2025) says "prompt injection attacks may never be totally mitigated". It calls an LLM an "inherently confusable deputy" and recommends "deterministic (non-LLM) safeguards that constrain the actions of the system", plus logging of tool calls. OWASP's Top 10 for LLM Applications 2025 files the same problem under LLM01:2025 Prompt Injection and LLM06:2025 Excessive Agency, and the second is the one engineering can act on.

Simon Willison explains why filters do not close the gap: "LLMs are unable to reliably distinguish the importance of instructions based on where they came from." A guardrail that stops 95% of attacks is "very much a failing grade".

SABER judges coding agents by the final state of a project workspace. Its tasks mix embedded injection with ordinary requests where the agent can take an unsafe shortcut or ignore a warning in the workspace, and its abstract reports that the best model tested still had "more than a 54% harmful safety-violation rate". That is a rate on the benchmark's tasks, not on your workload. It does undercut the hope that a well-behaved model keeps a workspace intact on its own.

The trifecta and the Rule of Two describe the same exposure

Willison's lethal trifecta is "Access to your private data", "Exposure to untrusted content" and "The ability to externally communicate". Remove one leg and the exfiltration path breaks.

Meta's Agents Rule of Two (31 October 2025) makes it a design rule. Until injection can be reliably detected, agents "must satisfy no more than two of the following three properties within a session": "process untrustworthy inputs", "access to sensitive systems or private data", "change state or communicate externally".

Meta's third property is wider. It counts any state change, so a CI agent with no internet but permission to push a branch still holds it.

Most rows in the incident table hold all three at once. That is the default shape of a coding agent.

The control moves to what the process can touch

You cannot reliably stop the model from being confused. You can decide, deterministically, what a confused process is able to do. The rest of this post is about that decision.

Isolation boundaries differ in what a kernel bug costs

Photo by Brett Sayles on Pexels

"Sandbox" covers mechanisms with very different failure modes. One question sorts them: if code inside finds a kernel bug, what does it get?

Containers, Landlock and bubblewrap share the host kernel

A container is a process wrapped in namespaces and cgroups. Every system call still lands in the host kernel. Docker's default seccomp profile "disables around 44 system calls out of 300+", which narrows the surface but leaves most of the kernel reachable.

Landlock "empowers any process, including unprivileged ones, to securely restrict themselves". It arrived in Linux 5.13 and grows by ABI version: TCP bind and connect from ABI 4, UDP only from ABI 10. It cannot restrict stat, chmod, chown or access, among others.

Bubblewrap builds a sandbox from user namespaces, an empty mount namespace and PR_SET_NO_NEW_PRIVS. Flatpak uses it, and so do Claude Code and Codex on Linux. Its README is blunt: "bubblewrap is not a complete, ready-made sandbox with a specific security policy." The policy is whatever the caller passes.

All three share one property: a kernel exploit from inside is a host compromise.

macOS Seatbelt is deprecated and everywhere

On macOS, Claude Code, Codex, Cursor and Gemini CLI all run commands under Seatbelt via sandbox-exec, with generated or built-in profiles. The sandbox-exec(1) man page that ships with macOS says "The sandbox-exec command is DEPRECATED" and points developers to the App Sandbox instead.

A whole product category depends on it anyway.

That is a platform risk to track, not a reason to turn the sandbox off.

gVisor and microVMs move the boundary

gVisor puts a user-space kernel, the Sentry, in front of the host. "No system call is passed through directly to the host", and the Sentry's own host calls "do not include the creation of new sockets" unless host networking is on. An attacker must compromise the Sentry before touching the host kernel. Since mid-2023 the default platform is systrap; ptrace is "no longer supported". The cost is stated plainly: transitions are "relatively expensive".

Kata Containers runs each workload in a lightweight VM with a dedicated kernel, on QEMU, Cloud Hypervisor or Firecracker. Firecracker is the minimal VMM behind Lambda and Fargate, described in the NSDI 2020 paper, and is "designed to be run securely inside an execution jail, set up by the jailer binary" per its getting-started guide.

In a microVM, a guest kernel bug compromises the guest. Reaching the host takes a hypervisor escape as well.

Wasmtime has the cleanest capability model: "All interaction with the outside world is done through imports and exports". But an agent running npm install and pytest needs a POSIX shell and native toolchains. In practice, Wasm suits generated functions, not shell-driving agents.

The boundaries side by side

Boundary A kernel bug inside reaches Main cost Used for agents by
Container + seccomp The host Shares the host kernel Gemini CLI (Docker, Podman), dev containers
Landlock The host Coverage depends on kernel ABI Cursor on Linux
bubblewrap The host Caller writes the policy Claude Code, Codex on Linux
macOS Seatbelt The host Deprecated interface Claude Code, Codex, Cursor, Gemini CLI
gVisor The Sentry first Slower syscalls Modal, Gemini CLI runsc option
microVM (Firecracker, Kata or other) The guest; the host needs a hypervisor escape Boot time, memory per VM E2B, Vercel, Cloudflare; Docker Sandboxes (hypervisor not documented)
Wasm A runtime bug, not a kernel bug No POSIX shell Not a fit for shell agents

If you run agent commands in a container yourself, this is a reasonable floor. It is assembled from documented flags in the docker run reference; the composition is ours.

# Linux host with Docker Engine and gVisor registered as the runsc runtime.
# One-time setup: sudo /usr/local/bin/runsc install && sudo systemctl reload docker
docker run --rm \
  --network none \
  --read-only --tmpfs /tmp \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --pids-limit 256 --memory 2g --cpus 2 \
  --user 1000:1000 \
  -v "$PWD":/work -w /work \
  -v "$PWD/.git":/work/.git:ro \
  --runtime=runsc \
  python:3.13-slim python script.py

With --network none, "only the loopback device is created" (docs). --runtime=runsc replaces the shared kernel with gVisor, per the install page. The workspace mount stays writable on purpose: it is the one thing the agent should change. .git is remounted read-only on top, so hooks and git config cannot be planted, and anything the host will run later (Makefile, package.json scripts, .vscode/) needs review before it runs.

Agent sandboxes ship with gaps their own docs list

Every major coding agent now ships a sandbox or a firewall. The question is what it covers out of the box, and the vendors' docs answer it.

Claude Code: off by default, and only for shell commands

Claude Code's sandbox uses Seatbelt on macOS and bubblewrap plus socat on Linux and WSL2. Native Windows commands run unsandboxed. It is off by default; /sandbox or sandbox.enabled: true turns it on.

It covers Bash, PowerShell and Monitor commands and their children. Built-in file tools, MCP servers, hooks, the statusline and apiKeyHelper run outside it. Once enabled, the defaults are:

  • writes: working directory and temp, with .claude settings, .bashrc, .gitconfig and .git hooks protected
  • reads: "Most of the machine, including credential files such as ~/.ssh and ~/.aws/credentials"
  • network: proxy only, allowedDomains starts empty; no UDP, QUIC or ICMP
  • environment: "Inherited from Claude Code, including any secrets in its environment"

A failed command can be retried outside the sandbox with dangerouslyDisableSandbox unless allowUnsandboxedCommands is false.

Anthropic reports that internally "sandboxing safely reduces permission prompts by 84%". That vendor-reported figure measures friction, not attacks stopped. The sandbox environments page is stricter: "Always run --dangerously-skip-permissions sessions inside a container, a VM, or the sandbox runtime."

Keys from the sandboxing docs (November 2026):

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false
  }
}

failIfUnavailable makes Claude Code exit at startup, instead of quietly running unsandboxed, on an unsupported platform or when bubblewrap or socat is missing. The same docs list sandbox.filesystem.denyRead, and sandbox.credentials.files and sandbox.credentials.envVars with deny or mask (file masking since v2.1.221), plus sandbox.network.allowedDomains with strictAllowlist (since v2.1.219; user, managed or CLI settings only). Use them to take ~/.ssh, ~/.aws and secret-named variables out of reach before anything else.

Codex: sandboxed by default, with two quiet gaps

Codex uses Seatbelt on macOS and bubblewrap on Linux, with a bundled fallback if bwrap is missing. "Linux uses bwrap plus seccomp by default", per the approvals page; write-ups describing a Landlock sandbox are out of date. In Git folders the default is workspace-write with on-request approvals, and "By default, the agent runs with network access turned off." .git, .agents, .codex and .aws stay read-only. The untrusted approval policy is retired and on-failure deprecated, so older configs may not mean what they say.

Gap one: domain rules in permission profiles are enforced only when features.network_proxy.enabled = true, which defaults to false, per the config reference. Network on without the proxy is direct and unfiltered.

Gap two: shell_environment_policy.ignore_default_excludes defaults to true, so variables with KEY, SECRET or TOKEN in their names reach the shell.

Adapted from the documented example, with the environment key from the config reference. The first keys pin the Git-folder defaults so they also hold outside Git folders; ignore_default_excludes = false is the change that matters.

# Codex config.toml (keys per learn.chatgpt.com docs, November 2026)
approval_policy   = "on-request"
sandbox_mode      = "workspace-write"
allow_login_shell = false   # do not source shell profiles for agent commands

[sandbox_workspace_write]
network_access = false

[shell_environment_policy]
ignore_default_excludes = false   # drop *KEY*, *SECRET*, *TOKEN* variables

Codex cloud splits by phase. The docs, now filed under "Codex Cloud (Legacy)", say "By default, Codex blocks internet access during the agent phase" (docs). In that legacy design, setup scripts got internet and secrets, and secrets were removed before the agent ran. In the current cloud environments, programs see a placeholder and a proxy substitutes the real secret for allowed destinations. The legacy presets were None, Common dependencies and All, optionally limited to GET, HEAD and OPTIONS.

The other agents mix firewalls and opt-in sandboxes

GitHub's coding agent became Copilot cloud agent, and from 28 October 2026 paid plans get the new Copilot in the cloud experience. Its firewall is on by default with a recommended allowlist. GitHub states the limits: "It does not apply directly to Model Context Protocol (MCP) server processes", setup steps are excluded, and "Sophisticated attacks may bypass the firewall, potentially allowing unauthorized network access and data exfiltration." The firewall is also "not compatible with self-hosted runners" or Windows.

Gemini CLI's sandbox is opt-in via -s, GEMINI_SANDBOX or tools.sandbox, backed by Seatbelt, Docker, Podman, gVisor or experimental LXC. The default Seatbelt profile, permissive-open, allows network access.

Cursor uses Seatbelt on macOS and Landlock plus seccomp on Linux, with the workspace in an overlay filesystem. It reports, as vendor figures, that sandboxed agents "stop 40% less often" and that about a third of requests on supported platforms run sandboxed.

Docker Sandboxes takes the other route and puts the agent in a microVM with a private Docker daemon. "All outbound TCP traffic from the sandbox routes through a proxy on your host", which swaps sentinel credential values for real ones outside the VM.

The security page lists the gaps. The default allowlist includes wildcards such as *.googleapis.com. The agent has sudo inside the VM. Local stdio MCP servers run on the host, which makes them part of the threat model covered in MCP beyond the hype. In direct mode, git hooks and Makefiles the agent writes can run later on the host.

Defaults at a glance

Agent Local mechanism Sandbox by default Network by default Secrets by default
Claude Code Seatbelt; bubblewrap on Linux; none on native Windows Off; shell commands only Unrestricted until the sandbox is on; then proxy, empty allowlist Reads ~/.ssh; inherits env secrets
Codex CLI Seatbelt; bwrap + seccomp On in Git folders Off; domain rules need the proxy flag Keeps KEY, SECRET, TOKEN vars
Codex cloud Hosted Hosted Blocked in agent phase Legacy: setup only; current: proxy placeholder
Copilot in the cloud Cloud sandbox or Actions runner Hosted Firewall; excludes MCP and setup Own session token
Gemini CLI Seatbelt, containers, gVisor Off; opt-in Default profile allows network Not stated in sandbox docs
Cursor Seatbelt; Landlock + seccomp About a third of requests (vendor, Feb 2026) Not stated Not stated
Docker Sandboxes microVM It is the sandbox Deny by default, broad built-in allowlist Sentinel swap at proxy

A sandbox that is off by default protects the people who were already careful.

Sandboxes now get their own escape advisories

Three escapes, all fixed:

  • Claude Code GHSA-7835-87q9-rgvv (CVE-2026-55607), June 2026. .git-named worktrees, a symlink and git's fsmonitor could overwrite ~/.zshenv and run code outside the macOS sandbox, after the victim cloned a malicious repo with injection content. Fixed in 2.1.163.
  • Codex GHSA-w5fx-fh39-j5rw (CVE-2025-59532), September 2025. A model-generated working directory became the writable root, so writes could land outside the session folder. Fixed in CLI 0.39.0 and IDE extension 0.4.12.
  • Cursor, whose advisory list includes a 2026 escape via Git hooks (GHSA-8pcm-8jpx-hv8r), a Critical one via failed path canonicalization (GHSA-3v8f-48vw-3mjx) and one via launching privileged containers (GHSA-v4xv-rqh3-w9mc).

Do not read advisory lists as a ranking. Volume reflects disclosure programs and bounties as much as code quality.

Configuration files are the escape route

Line the advisories up against the incidents and the same files recur. Copilot's .vscode/settings.json. Cursor's mcp.json. Codex's project config.toml. Claude Code's settings.json (GHSA-ff64-7w26-62rf, "Sandbox Escape via Persistent Configuration Injection in settings.json", fixed in 2.1.2). Git hooks and ~/.zshenv.

Each is read and acted on later, usually outside the sandbox. The products' write-protected lists now cover those files: Claude Code guards .claude settings and .git hooks, Codex keeps .codex and .git read-only, and Cursor's example sandbox policy denies writes to .vscode, .cursor and .git/hooks.

Any file a later, unsandboxed process will execute is part of the sandbox boundary.

Patch the sandbox like a browser

Agent CLIs release often, and the sandbox ships inside them. Pinning an agent version also pins its sandbox bugs. Manage agent tools as fleet software with a minimum version and an owner who reads the advisories, the same discipline vulnerability response is becoming a 24-hour operational duty argues for on the vendor side.

Hosted sandbox startup numbers need a definition

Products that run agents for their users rent hosted sandboxes, and three of the best-known platforms use Firecracker microVMs.

The isolation is mostly Firecracker

E2B: "Every E2B sandbox runs in its own Firecracker microVM with its own kernel". Vercel: a "secure Firecracker microVM with its own filesystem and network". Cloudflare: "Each container instance runs in a Firecracker microVM with its own kernel and network". Modal runs gVisor or a VM runtime. On Kubernetes, agent-sandbox adds a Sandbox resource that delegates isolation to a gVisor or Kata RuntimeClass, plus a SandboxWarmPool.

A warm pool is also where most fast startup numbers come from.

Firecracker's number is to guest init

Firecracker's specification commits to "<= 125 ms" from the InstanceStart call to the guest's /sbin/init, with a minimal kernel, no serial console, on metal instances, enforced by integration tests.

That is a strong result. Guest init is not "sandbox ready".

At /sbin/init no language runtime is warm, no repo is cloned and no SDK is connected. Vercel says its sandboxes "start in milliseconds", a vendor claim. Figures in that range generally come from snapshot restore or a warm pool, not a cold boot. Ask of any startup number:

  • cold boot, snapshot restore or warm pool
  • time to what: guest init, SDK connected, first command
  • at what concurrency

Persistence changes the threat model

E2B defaults to a five-minute timeout, up to 24 hours on Pro, and paused sandboxes keep state "indefinitely". Modal allows up to 24 hours plus snapshots. Vercel auto-saves state on stop. Docker Sandboxes persist until sbx rm. Whatever an injected instruction planted survives into the next session unless something resets it.

Platform Isolation Startup claim Egress by default Lifetime
E2B Firecracker microVM Not published in docs Internet on 5 min default, 24 h max (Pro); pause keeps state
Modal gVisor or VM Not published Public IPs allowed 5 min default, 24 h max
Vercel Sandbox Firecracker microVM "milliseconds" (vendor) Configurable: open, deny-all, granular Auto-save; snapshots
Cloudflare Containers Firecracker microVM Not published Internet on; enableInternet: false available See docs

Fast startup is what makes a fresh sandbox per task affordable. Use it for that.

Egress is where the data leaves

The network is the trifecta leg a sandbox can cut most cleanly, and the defaults leave it partly open.

Default deny needs a proxy, and the proxy needs a policy

Codex starts closed, and so does Claude Code's sandbox once it is enabled. Docker Sandboxes denies by default but ships a broad built-in allowlist. E2B's internet is on by default, Modal allows outbound to public IPs, Gemini CLI's default profile allows the network, and Daytona's tiers 3 and 4 get full internet.

Opening the network selectively takes a proxy, and the strongest designs give the sandbox no direct route. On Linux, Anthropic's sandbox runtime runs commands without network access and bind-mounts HTTP and SOCKS5 proxies in over Unix sockets. The proxy resolves hostnames itself and refuses loopback, link-local and cloud metadata ranges.

Hostname allowlists trust the client

Most allowlists decide on a name the client supplies. Claude Code's docs say so:

"Allowing broad domains such as github.com can create paths for data exfiltration. Because the proxy makes its allow decision from the client-supplied hostname without inspecting TLS, code running inside the sandbox can potentially use domain fronting..."

Modal's beta domain allowlist matches by SNI without decryption and carries the same caveat. E2B matches Host or SNI, filters other ports by CIDR only, and cannot filter QUIC by domain. Vercel inspects SNI, can terminate TLS selectively, and applies domain rules to DNS.

Claude Code also has an experimental tlsTerminate option. Without TLS termination, a domain allowlist checks the name on the envelope.

Allowlisted domains are exfiltration channels

An honest hostname check still leaves the question of what the allowed host accepts. PromptArmor's May 2026 analysis covers the usual entries:

  • npm: publish a package an attacker can read
  • github.com: push to a public repo or gist, or use GitHub as command and control
  • docs.google.com: submit a prefilled Form with one GET request
  • *.amazonaws.com: matches attacker.s3.amazonaws.com

The legacy Codex cloud docs show a hidden instruction in a GitHub issue piping the last commit message to an external server, and suggest allowing only GET, HEAD and OPTIONS. That helps against uploads. The Forms case shows a GET can still carry data in its URL.

E2B has a sharper trap: adding a domain to allowOut automatically allows 8.8.8.8 for all traffic. "Deny everything except one API" also opens one public resolver on every port.

DNS is a network

Zenity found HTTP blocked but DNS resolvable in Claude's code-execution sandbox, enough for exfiltration and a shell over TXT records. Reported in November 2025 and fixed, it returned in May 2026: "A newer deployment had brought the problem back to life." Zenity calls it "Claude's sandbox" and does not say whether Claude Code is affected. Mindgard showed Cline leaking keys through ping, which needed no approval. Modal states that "DNS traffic is allowed without restriction except when all outbound traffic is blocked".

The Zenity regression is the lesson. Egress rules are configuration, configuration drifts, and a fixed leak can return in a later deployment. Test egress from inside the sandbox on a schedule.

Credentials should never enter the sandbox

Egress controls limit where stolen data goes. The better move is to keep credentials out of reach entirely.

Inherited environments are the default leak

The defaults run the wrong way. Claude Code's sandbox inherits secrets and can read ~/.aws/credentials. Codex passes secret-named variables. An agent that can read a credential can be told to send it somewhere.

Inject secrets at the proxy

Two patterns recur across vendors. Either the sandbox holds a placeholder or nothing and a proxy outside the boundary attaches the real credential on the way out, or the secret exists only in a phase the agent never runs in.

  • Claude Code cloud: a custom proxy validates a scoped credential and the push target branch, then attaches the real git token
  • Claude Code local: credentials rules that mask files and variables
  • Docker Sandboxes: sentinel value swapped at the host proxy
  • Vercel Sandbox: host-side header transform
  • E2B: Secret.fill for HTTPS requests to a chosen host
  • Codex cloud: a proxy-substituted placeholder today; setup-phase-only secrets in the legacy design

This excerpt from Vercel's August 2026 post (Vercel Sandbox SDK) injects a bearer token for one host, then cuts the network:

const sandbox = await Sandbox.create({
  networkPolicy: { allow: { "ai-gateway.vercel.sh": [{ transform: [{ headers: {
    "Authorization": `Bearer ${process.env.AI_GATEWAY_TOKEN}` } }] }] } }
});
await sandbox.update({ networkPolicy: 'deny-all' });

The token lives in the orchestrator, not the microVM. Code inside can call the gateway but cannot read the header. Header transforms are on Vercel's Pro and Enterprise plans, and deny-all also blocks DNS resolution.

Injection is not scoping. E2B documents two caveats for Secret.fill: every process sending HTTPS to that host gets the secret, and a missing secret fails open. Injected code can still use a credential through the proxy. That is why Anthropic's proxy also checks the branch.

Make the tokens that do enter short-lived

GitHub Actions OIDC tokens are "only valid for a single job", with no stored secret. Vercel recommends its OIDC token for Sandbox auth. The same move for package publishing is covered in software publishing is moving beyond long-lived tokens; Nx and Cline both lost long-lived npm tokens.

CI puts outsider text and release credentials in one job

Photo by Miha Meglic on Unsplash

A CI job triggered by issues or pull requests can hold all three Rule of Two properties at once: it reads outsider text, holds secrets, and can push, publish and write caches. Add an agent with a shell and the rule is broken before the first prompt.

Clinejection, step by step

Adnan Khan's write-up (9 February 2026) is the most complete public chain, from an issue title to release tokens.

  1. Cline's issue triage workflow, live from 21 December 2025, ran claude-code-action with --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch".
  2. allowed_non_write_users: "*" let anyone who could open an issue trigger it.
  3. The issue title was interpolated into the prompt.
  4. An injected title could induce npm install of attacker code in the triage job.
  5. Actions cache poisoning could reach the nightly release workflow, exposing VSCE_PAT, OVSX_PAT and NPM_RELEASE_TOKEN.

Khan reported it on 1 January 2026 and, by his account, got no substantive response until public disclosure, after which a fix landed within roughly 30 minutes. On 17 February a different actor used an npm token that had not been properly revoked to publish cline@2.3.0 with a postinstall running npm install -g openclaw@latest. Cline's statement, as quoted by Khan, said "The only modification was an added postinstall script"; 2.4.0 fixed it (GHSA-9ppg-jx86-fqw7).

An agent with Bash, an input anyone could write, a shared cache and a long-lived token were enough.

Aikido's PromptPwnd (December 2025) shows it is a pattern: issue and PR text reaching agents with privileged tokens via the Gemini CLI action, Claude Code Actions, OpenAI Codex Actions and GitHub AI Inference. Google patched gemini-cli within four days. Aikido's "At least 5 Fortune 500 companies are impacted" is vendor-reported, with no names.

Hardening an agent job in GitHub Actions

A skeleton for an issue-triage agent. The agent script is a placeholder; the workflow syntax is standard, and the checkout pin is actions/checkout v7.0.1.

name: issue-triage

on:
  issues:
    types: [opened]

permissions: {}

jobs:
  triage:
    runs-on: ubuntu-24.04
    timeout-minutes: 15
    permissions:
      contents: read
      issues: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - name: Run the triage agent (placeholder script)
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          GH_TOKEN: ${{ github.token }}
          MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}
        run: ./scripts/triage-agent.sh
  • permissions: {}, then only what the job needs; this token can label and comment, not push
  • persist-credentials: false, so no token is left in .git/config
  • issue text through env, never ${{ }} inside run:; this stops shell injection, not prompt injection
  • no Bash, Write or WebFetch for triage
  • no release or cloud secrets in workflows outsiders can trigger
  • the model API key is the only secret in the job: scoped to this workflow, with a spend limit, granting nothing else
  • release jobs that do not restore caches untrusted jobs can write
  • release credentials behind a protected environment with reviewers, and OIDC publishing instead of a stored token
  • for Copilot sessions on Actions runners, ephemeral single-use runners, as GitHub recommends

A triage bot should be able to label an issue. That is all.

Sandbox policy should follow input trust and reach

No single boundary fits every agent. Each step up in untrusted input should come with a step down in reach.

Tier Typical input Minimum boundary Network Credentials
Local interactive Your code, docs you chose Built-in sandbox on, approvals on Proxy, short allowlist Deny reads of ~/.ssh, ~/.aws; strip secret vars
Local autonomous Same, plus whatever it fetches Container, VM, Docker Sandboxes or sandbox runtime Default deny None inside; proxy injection
CI Issues, PRs, third-party text Ephemeral runner, minimal job token Only what the job needs OIDC; no release secrets on untrusted triggers
Hosted multi-tenant Arbitrary user input microVM or gVisor per session Deny, changed per phase Proxy injection only

Step one: inventory and turn on what ships

  • every agent CLI and extension, with versions
  • every workflow that invokes an agent, and its trigger
  • every MCP server, and whether it runs inside the sandbox
  • Claude Code: sandbox.enabled, failIfUnavailable, allowUnsandboxedCommands: false
  • Codex: ignore_default_excludes = false; the proxy flag before any domain rules
  • Gemini CLI: a sandbox enabled, with a profile chosen deliberately rather than the network-open default

Step two: close the network, then open it narrowly

  • default deny for the agent phase
  • registries open during setup only, then deny
  • no general-purpose hosts like github.com or *.amazonaws.com unless the task needs them
  • DNS resolved by the proxy, or blocked with the rest of egress
  • egress tested from inside, on a schedule

Step three: take credentials out and treat config as code

  • no long-lived tokens the agent can read
  • proxy-side injection, scoped to one host, branch or job
  • OIDC for cloud and registry access from CI
  • agent settings, MCP config, editor settings, git hooks and shell startup files write-protected and reviewed like workflow changes
  • persistent sandboxes reset between untrusted tasks

AI coding tools used to stop at a suggestion. A person read it and decided whether it ran.

Now the agent runs it, often before anyone reads it, and the text that shaped it may have come from an issue title or an install script. Prompt injection will keep working some of the time. The sandbox decides what that costs: which kernel the code shares, which hosts it reaches, which secrets it reads, which files it leaves for the next unsandboxed process.

The tools are namespaces, microVMs, proxies and short-lived tokens, applied to a new kind of process. Treat the agent as untrusted code with a helpful interface, and most of its incidents become boring.

References

Incident reports and advisories

  1. Invariant Labs, GitHub MCP toxic agent flow disclosure, 26 May 2025: https://invariantlabs.ai/blog/mcp-github-vulnerability
  2. Tracebit, Gemini CLI allowlist bypass and README injection, 28 July 2025: https://tracebit.com/blog/code-exec-deception-gemini-ai-cli-hijack
  3. AWS Security Bulletin AWS-2025-015, CVE-2025-8217 (Amazon Q Developer for VS Code), 23 July 2025: https://aws.amazon.com/security/security-bulletins/AWS-2025-015/
  4. The Register, Amazon Q prompt report, 24 July 2025: https://www.theregister.com/2025/07/24/amazon_q_ai_prompt/
  5. Cursor security advisories (GHSA-4cxx-hrm3-49rm, GHSA-8pcm-8jpx-hv8r, GHSA-3v8f-48vw-3mjx, GHSA-v4xv-rqh3-w9mc and others): https://github.com/cursor/cursor/security/advisories
  6. Tenable, FAQ on CurXecute and MCPoison: https://www.tenable.com/blog/faq-cve-2025-54135-cve-2025-54136-vulnerabilities-in-cursor-curxecute-mcpoison
  7. Check Point Research, Cursor MCPoison (CVE-2025-54136), 5 August 2025: https://research.checkpoint.com/2025/cursor-vulnerability-mcpoison/
  8. Johann Rehberger, GitHub Copilot RCE via prompt injection (CVE-2025-53773), 12 August 2025: https://embracethered.com/blog/posts/2025/github-copilot-remote-code-execution-via-prompt-injection/
  9. Nx, GHSA-cxm3-wv7p-598c, 27 August 2025: https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c
  10. Socket, Nx package compromise analysis: https://socket.dev/blog/nx-packages-compromised
  11. Snyk, weaponizing AI coding agents in the Nx malicious package: https://snyk.io/blog/weaponizing-ai-coding-agents-for-malware-in-the-nx-malicious-package
  12. Check Point Research, Codex CLI command injection (CVE-2025-61260), 1 December 2025: https://research.checkpoint.com/2025/openai-codex-cli-command-injection-vulnerability/
  13. OpenAI Codex, GHSA-w5fx-fh39-j5rw (CVE-2025-59532), 19 September 2025: https://github.com/openai/codex/security/advisories/GHSA-w5fx-fh39-j5rw
  14. Anthropic Claude Code, GHSA-ff64-7w26-62rf (CVE-2026-25725): https://github.com/anthropics/claude-code/security/advisories/GHSA-ff64-7w26-62rf
  15. Anthropic Claude Code, GHSA-7835-87q9-rgvv (CVE-2026-55607), 25 June 2026: https://github.com/anthropics/claude-code/security/advisories/GHSA-7835-87q9-rgvv
  16. Aikido Security, PromptPwnd, 4 December 2025: https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents
  17. Adnan Khan, Clinejection, 9 February 2026: https://adnanthekhan.com/posts/clinejection/
  18. Cline, GHSA-9ppg-jx86-fqw7: https://github.com/cline/cline/security/advisories/GHSA-9ppg-jx86-fqw7
  19. Anthropic, report on an AI-orchestrated espionage campaign, 13 November 2025: https://www.anthropic.com/news/disrupting-AI-espionage
  20. Zenity Labs, DNS exfiltration in Claude's sandbox, 27 August 2026: https://labs.zenity.io/post/it-s-always-dns-in-claude-s-sandbox-from-data-exfiltration-to-a-bidirectional-dns-shell
  21. Mindgard, Cline data exfiltration via prompt injection and DNS, 18 November 2025: https://mindgard.ai/disclosures/cline-bot-ai-coding-agent-data-exfiltration-via-prompt-injection-and-dns
  22. PromptArmor, allowlist domain analysis, 13 May 2026: https://www.promptarmor.com/resources/what-domains-should-i-add-to-my-allowlist

Guidance, frameworks and research

  1. UK NCSC, Prompt injection is not SQL injection (it may be worse), 8 December 2025: https://www.ncsc.gov.uk/blog-post/prompt-injection-is-not-sql-injection
  2. Simon Willison, the lethal trifecta, 16 June 2025: https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/
  3. Meta AI, Agents Rule of Two, 31 October 2025: https://ai.meta.com/blog/practical-ai-agent-security/
  4. OWASP, Top 10 for LLM Applications 2025: https://genai.owasp.org/llm-top-10/
  5. SABER, arXiv:2606.01317: https://arxiv.org/abs/2606.01317
  6. Anthropic Engineering, Beyond permission prompts: making Claude Code more secure and autonomous, 20 October 2025: https://www.anthropic.com/engineering/claude-code-sandboxing

Agent product documentation

  1. Claude Code, Sandboxing: https://code.claude.com/docs/en/sandboxing
  2. Claude Code, Sandbox environments: https://code.claude.com/docs/en/sandbox-environments
  3. Anthropic sandbox runtime: https://github.com/anthropics/sandbox-runtime
  4. OpenAI Codex, Sandboxing: https://learn.chatgpt.com/docs/sandboxing
  5. OpenAI Codex, Agent approvals and security: https://learn.chatgpt.com/docs/agent-approvals-security.md
  6. OpenAI Codex, Configuration reference: https://learn.chatgpt.com/docs/config-file/config-reference.md
  7. OpenAI Codex cloud, Internet access: https://learn.chatgpt.com/docs/cloud/internet-access.md
  8. GitHub Docs, Copilot in the cloud (coding agent) overview: https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent
  9. GitHub Docs, Customize the agent firewall: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/customize-the-agent-firewall
  10. GitHub Docs, Customize the agent environment: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/customize-the-agent-environment
  11. Gemini CLI, Sandboxing: https://geminicli.com/docs/cli/sandbox/
  12. Cursor, agent sandboxing post, 18 February 2026: https://cursor.com/blog/agent-sandboxing
  13. Docker, Sandboxes architecture: https://docs.docker.com/ai/sandboxes/architecture/
  14. Docker, Sandboxes security: https://docs.docker.com/ai/sandboxes/security/

Isolation technology

  1. Docker, Seccomp security profiles: https://docs.docker.com/engine/security/seccomp/
  2. Docker, None network driver: https://docs.docker.com/engine/network/drivers/none/
  3. Docker, docker container run reference: https://docs.docker.com/reference/cli/docker/container/run/
  4. Linux kernel documentation, Landlock: https://docs.kernel.org/userspace-api/landlock.html
  5. Bubblewrap: https://github.com/containers/bubblewrap
  6. Apple, sandbox-exec(1) man page, shipped with macOS (man sandbox-exec)
  7. gVisor, Security model: https://gvisor.dev/docs/architecture_guide/security/
  8. gVisor, Platforms: https://gvisor.dev/docs/architecture_guide/platforms/
  9. gVisor, Installation: https://gvisor.dev/docs/user_guide/install/
  10. Kata Containers: https://katacontainers.io/
  11. Firecracker, SPECIFICATION.md: https://github.com/firecracker-microvm/firecracker/blob/main/SPECIFICATION.md
  12. Firecracker, Getting started: https://github.com/firecracker-microvm/firecracker/blob/main/docs/getting-started.md
  13. Agache et al., Firecracker: Lightweight Virtualization for Serverless Applications, NSDI 2020: https://www.usenix.org/conference/nsdi20/presentation/agache
  14. Wasmtime, Security: https://docs.wasmtime.dev/security.html

Hosted sandbox platforms and credentials

  1. E2B, Security: https://e2b.dev/security.md
  2. E2B, Internet access: https://docs.e2b.dev/network/internet-access.md
  3. E2B, Sandbox lifetime: https://docs.e2b.dev/faq/sandbox-lifetime.md
  4. Modal, Sandboxes: https://modal.com/docs/guide/sandbox
  5. Modal, Sandbox networking: https://modal.com/docs/guide/sandbox-networking
  6. Vercel, Vercel Sandbox: https://vercel.com/docs/vercel-sandbox
  7. Vercel, network boundary post, 11 August 2026: https://vercel.com/blog/a-sandbox-without-a-network-boundary-is-only-half-a-sandbox
  8. Cloudflare, Containers architecture: https://developers.cloudflare.com/containers/platform-details/architecture/
  9. Cloudflare, containers package README (enableInternet): https://github.com/cloudflare/containers
  10. Daytona, Network limits: https://www.daytona.io/docs/en/network-limits/
  11. kubernetes-sigs/agent-sandbox: https://github.com/kubernetes-sigs/agent-sandbox
  12. GitHub Docs, OpenID Connect: https://docs.github.com/en/actions/concepts/security/openid-connect

No comments yet. Be the first to comment.