Beyond Git Worktree

Sandbox Security for Agents Running Untrusted Generated Code

Isolating untrusted agent code prevents credential leaks and supply chain attacks.

Senior Writer · · 14 min read
Cover illustration for “Sandbox Security for Agents Running Untrusted Generated Code”
Sandbox Infrastructure · September 4, 2026 · 14 min read · 3,107 words

Coding agents no longer just suggest what to type. They run shells, install packages, edit repositories, execute tests, and open pull requests, often with the same permissions as the developer who invoked them. That's the entire problem this piece addresses: once an agent executes code instead of merely proposing it, the question stops being whether the model is good and becomes what happens when it's wrong, or manipulated, inside a live environment. Most teams treat sandboxing as an optional hardening step to bolt on after the agent already works, and that sequencing is backwards.

The failure mode has a name in the security literature: the confused deputy problem. A process with legitimate authority gets tricked into using that authority on behalf of an attacker. An LLM-based agent is a textbook deputy. It holds real credentials, real filesystem access, real network reach, and it follows instructions, some of which it reads out of a file, a webpage, or a tool response rather than from the developer who trusts it. Role-based access control doesn't catch this, because RBAC checks who is making a request, not whether the instruction behind that request actually came from someone authorized to give it. The agent's identity is valid; the intent behind its next action might not be.

Handing an agent tool access is like handing a contractor the keys to a building. The contractor can be competent, vetted, even excellent at the job, and none of that changes the fact that anything the contractor touches, builds, or wires up is unverified until someone checks it. Agent-generated code is untrusted code by definition, independent of how capable the underlying model is. Without isolation, the exposure is concrete: host filesystem access, credential leakage, supply chain compromise through a poisoned dependency, and unreviewed changes landing in production. Each layer covered below closes off one specific way that exposure turns into damage, and each one carries its own share of the job. Treating any one layer as sufficient on its own is the mistake that keeps producing incidents.

How attack surface expands as agents gain tool access

The shift from autocomplete to autonomous execution didn't happen gradually. Agents now operate across repositories, terminals, browsers, package managers, test runners, and pull request workflows, often stitched together in a single session. Every one of those capabilities is a door, and most teams open all of them at once because turning one off feels like it defeats the point of having an agent. The point of an agent, though, is what it accomplishes inside a boundary, not how many doors it can open.

Shell access means arbitrary command execution. Package manager access means dependency poisoning sits one npm install away. Filesystem access means secrets sitting in environment variables or config files are readable by anything the agent runs. External API calls open a path for data exfiltration or lateral movement into systems the agent was never meant to touch. PR creation means unreviewed code can reach a merge queue without a human ever looking hard at the diff.

Model Context Protocol tool hijacking makes this concrete. A malicious tool gets published looking exactly like a benign utility, say a JSON formatter, but carries hidden instructions that fire the moment the agent invokes it. Those instructions run with whatever permissions the agent process already holds: broad filesystem read and write, environment variables full of API keys, sometimes internal network access. The tool doesn't need to break anything. It just needs to ask nicely, in the agent's own voice.

This isn't speculative. The 2026 Snowflake Cortex Code CLI incident, disclosed by PromptArmor, showed indirect prompt injection combined with weak command validation letting AI-generated instructions bypass human approval and escape the sandbox entirely, reaching cached credentials through arbitrary code execution. That's the failure mode above, observed in a shipped product rather than sketched out as a hypothetical.

What makes this hard to reason about is composition. An agent that can legitimately read secrets for configuration purposes, and can also legitimately open external pull requests, has, in sequence, a working credential-exfiltration pipeline. Neither capability is dangerous on its own; together, they're a path. Attackers have noticed where the leverage sits: the overwhelming majority of npm maintainer account takeovers ever recorded happened in 2025 alone. Package registries are now a high-value target, and AI coding agents offer a short, fast route from a poisoned package straight into production.

The four isolation layers and what each one actually stops

No single isolation mechanism covers every failure mode above, and treating one as a stand-in for the others is where most designs go wrong. These layers aren't redundant copies of the same protection. Each addresses a distinct class of threat, and skipping one doesn't get compensated by doubling down on another.

Layer one is the ephemeral execution environment: nothing carries over between sessions, and anything the agent touches gets discarded when the session ends. Layer two is syscall restriction, which constrains what the executing code is allowed to ask the operating system to do, regardless of what the application logic or the agent's own instructions say. Layer three is network control, which governs what the agent can reach, blocking both data exfiltration and lateral movement toward internal services. Layer four is scoped, ephemeral credentials, which limit what the agent is actually authorized to do even if the first three layers somehow get bypassed.

The stacking matters because failures compose just as badly as capabilities do. A script that gets cut off mid-execution inside a syscall-restricted sandbox, but with persistent state and a broad, long-lived credential, still leaves infrastructure in a condition nobody planned for. Container and VM isolation enforce boundaries at runtime, but they don't guarantee atomicity: a half-finished operation can leave the surrounding state inconsistent no matter how tight the kernel boundary is. Whether that mess is even recoverable depends on the environment being ephemeral and the tool calls being logged, not on isolation alone. Each section that follows takes one of these four layers in turn, and the final section covers how they're observed and governed once all four are running together.

Choosing the right isolation technology for the execution environment

Four isolation technologies dominate current practice, and they sit at different points on a tradeoff curve between strength and overhead. Standard containers belong nowhere near untrusted agent code, and that's worth saying without hedging: the shared kernel underneath Docker and runc is a single point of failure, and no amount of careful configuration changes that fact.

CVE-2024-21626, nicknamed "Leaky Vessels," was a file descriptor leak in the runc runtime that allowed container escape and host filesystem access. It's concrete proof that shared-kernel isolation gives way under the right conditions.

gVisor takes a different approach: a user-space kernel intercepts syscalls before they ever reach the host kernel. Modal builds its Sandboxes product on gVisor, layering custom logic on top to block system calls associated with known malicious behavior. It's a reasonable, lighter-weight option for some threat models, but it isn't the strongest boundary available, and it shouldn't be mistaken for one.

MicroVMs, Firecracker and Kata Containers among them, give each workload a dedicated kernel. Current security guidance treats this as the minimum acceptable bar for production agent execution, and that bar is the one to hold teams to. If one session's kernel gets compromised, it has no path to an adjacent workload's kernel, because there isn't one to reach.

WebAssembly with capability-based security is the fourth path. The Cosmonic and wasmCloud model runs agent-generated code inside sandboxed Wasm components that have no access to files, network, or keys unless a capability is explicitly granted at instantiation. There's no ambient authority to abuse, because nothing is granted by default.

The decision rule is simple even where the tradeoffs aren't: standard containers are off the table for untrusted code, full stop, and microVMs are the conservative baseline for anything touching production. Beyond that baseline, the choice depends on threat model and performance budget. What matters just as much as the technology itself is a design property sitting underneath all four: the environment has to be built fresh for every session and thrown away when the session ends. The isolation technology sets how hard the boundary is; ephemerality decides whether anything survives to leak across sessions in the first place.

Syscall filtering and why application-level sandboxing is not enough

The principle here is narrow and worth stating precisely: syscall filtering restricts what executing code can ask the operating system to do, independent of application-level logic and independent of whether the agent decides to follow its own instructions. Application-level limits alone fail because agent-generated code can simply route around them. A design that stops at the application layer and calls the job done is asking the untrusted code to police itself, which is a weak foundation for a boundary.

Resource limits have to be enforced at the cgroup level to mean anything. That's why OWASP's Top 10 for LLM Applications, in its 2025 revision, classifies unbounded resource consumption as its own named risk category, LLM10:2025, rather than folding it into something broader.

A few mechanisms do this well. seccomp-bpf allowlists a fixed set of syscalls and denies everything else by default, so a Python script with no legitimate reason to open a raw socket simply can't, no matter what the code inside it says. gVisor's interception model achieves something similar by catching syscalls before they hit the host kernel and applying custom blocking rules for patterns tied to known attacks. Wasm's capability model goes further still: a module can't invoke a capability it wasn't explicitly handed at instantiation, so there's no ambient authority sitting around waiting to be misused. Restricted Python interpreters, which limit available syntax and built-ins, cut the syscall surface down before execution even starts.

There's a moving target underneath all of this, and it deserves naming directly. Frontier models' success rate on apprentice-level cybersecurity tasks rose from under 10% in late 2023 and early 2024 to roughly half by 2025, with the first expert-level task completion recorded that same year. A syscall policy tuned for what models could do in 2023 is not a policy suited to what they can do now. This isn't a set-and-forget configuration; it needs the same maintenance discipline as any other security control facing an improving adversary.

None of this touches what the agent is authorized to do through legitimate API calls or valid credentials. Syscall filtering constrains the code's conversation with the operating system, but it has nothing to say about the agent's conversation with everything else, which is where the credential layer picks up.

Network controls as a containment boundary, not an afterthought

Default-open networking is the wrong starting assumption for an agent sandbox, and most teams get this backwards by treating egress rules as something to tighten later, after the workflow already works. By then the habit is set and nobody wants to be the one who breaks the pipeline to fix it. An agent that can reach both the public internet and internal services in the same session has, in one sitting, both an exfiltration channel and a lateral movement path.

Those are genuinely two different threats. Exfiltration looks like an agent reading a secret off the filesystem or out of an environment variable, then making an outbound call to an endpoint that has nothing to do with the task. Lateral movement looks like agent code reaching internal APIs, databases, or message queues that happen to be reachable from the execution environment, even though nothing about the task called for them.

The practical policy set is short. Egress should be allowlist-only: the agent reaches specific package registries, the model provider's API, and a defined set of external endpoints, nothing beyond that. Inbound connections should be denied by default, since a sandbox running someone else's code has no business acting as a server. The sandbox's network segment needs to sit apart from internal services entirely; the execution environment gets treated as an untrusted zone on the network map, not a trusted extension of it.

The MCP tool hijacking scenario from earlier makes the stakes plain. Without egress controls in place, a hijacked tool can beacon straight out to an attacker's server carrying whatever credentials it managed to harvest. Network policy is the backstop that holds even when the tool itself turns out to be the attacker.

The scale of what's already leaking underscores why that backstop matters. According to reporting by techinformed.com, 28.65 million new hardcoded secrets were added to public GitHub commits in 2025, up 34% year-over-year, and 1,275,105 of those were AI-service secrets specifically, up 81%. Credentials are already leaking at scale before any of this reaches an agent; network controls limit what an attacker can do with a leaked credential once an agent-shaped door has been pried open, even though they can't stop the leak itself. One honest caveat: egress that's too tight breaks real workflows, dependency installs, calls to external services the task actually needs. Network policy has to be set per task, not applied as one blanket rule across every agent in the fleet.

Scoped, ephemeral credentials and why persistent tokens are the wrong default

The common failure mode isn't exotic. Agents get handed long-lived, broadly-scoped tokens because that's the fastest way to get something working, and then that same token gets reused across sessions, stored in an environment variable, and left to sit there unrotated for months. Nobody decides to build it this way. It just accumulates, one shortcut at a time, until the token has more reach than anyone can account for.

This is the norm, not the exception. The Tenable Cloud and AI Security Risk Report 2026 found that 52% of non-human identities hold critical excessive permissions, meaning over half. Overpermissioned agent credentials are the default state of most fleets running today, and any argument that treats broad, persistent tokens as a reasonable starting point is arguing against the evidence.

The correct pattern is least privilege enforced at the credential level, scoped per task. Temporary credentials get minted at session start, covering exactly what that task requires and nothing more. They get revoked the moment the session ends, not on some periodic rotation schedule but immediately on completion. When an agent spins up a sub-agent for a delegated task, that sub-agent should receive an attenuated credential; a sub-agent generating tests has no business inheriting deployment authority just because its parent had it.

Most orchestration frameworks don't do this out of the box, for a mundane reason: ephemeral credential issuance requires wiring into an identity provider, whether that's AWS STS, Vault, or a workload identity system, and that's infrastructure most teams skip when they're standing up an agent experiment quickly. The governance picture behind this is worse than the technical gap suggests. A CSA survey of 228 IT and security professionals, published in March 2026, found that 68% of organizations can't clearly distinguish between human and AI agent activity in their systems, and only 18% are confident their IAM systems can manage agent identities at all. Most teams don't just lack the discipline to scope credentials properly; they lack the visibility to know whether they're doing it, even when they intend to. Credential scope belongs at agent configuration time, decided by the people building the system and reviewed the way any other infrastructure change gets reviewed, rather than left for the agent to decide at runtime.

Human-in-the-loop controls and where to place them in the execution flow

Some categories of action can't be safely automated away no matter how good the sandbox is: changes to production infrastructure, modifications to security configurations, merges into protected branches, access to regulated data. Technical isolation reduces the blast radius of a mistake; it doesn't make the mistake acceptable.

Graduated permission models are the right shape for this. Claude Code, for instance, exposes permission modes ranging from fully autonomous execution down to mandatory approval on every single tool call, which means the same agent can run at very different trust levels depending on the task and how much risk it carries. That trust level belongs in configuration, set deliberately ahead of time, not something the agent adjusts for itself mid-run.

Where the approval gate sits in the flow matters as much as whether it exists. Pre-execution checks, static analysis of a planned action before anything runs, catch the obviously dangerous moves before they happen at all. Mid-execution checks pause and escalate when the agent's next planned step exceeds whatever scope it was configured with. Post-execution review, PR-style inspection of diffs and tool call logs before anything merges, catches what slipped past the earlier gates.

The regulatory angle isn't optional in a lot of industries. When an agent writes code that governs credit approval, anti-money-laundering screening, or customer onboarding, the institution stays on the hook for whatever ends up running in production, regardless of whether a human or an agent wrote the line that broke. The OWASP Top 10 for Agentic Applications, published in December 2025, formalizes goal hijacking, tool misuse, and human-agent trust exploitation as named risk categories in their own right, and human-in-the-loop review is the primary mitigation OWASP lists against that last one.

Over-gating is a real failure mode too, and it deserves one honest sentence: requiring a human to sign off on every single agent action erases the entire productivity case for using an agent in the first place. The goal is risk-calibrated gating, tuned to what's actually dangerous, not blanket intervention applied out of caution that never gets revisited.

Observability and audit trails as a structural requirement, not instrumentation debt

Every layer above only holds up if someone can prove, after the fact, what actually happened during a session. That structural role is easy to underrate, and treating it as optional instrumentation is the same mistake as treating the sandbox itself as optional. A production agent session needs, at minimum, a record of every tool call in sequence with its inputs and outputs, every diff the agent produced attributed back to the session and to whatever triggered it, whether that was a human, a schedule, or an external event, and a record of exactly which credentials were issued and when they were revoked.

Without that record, containment turns into guesswork after an incident instead of a documented fact before one. Isolation stops the damage from spreading; audit trails are what let anyone say, with confidence, exactly what was contained and exactly what wasn't. Skipping this layer doesn't just weaken visibility. It quietly undermines the credibility of every other layer built on top of it, since a boundary nobody can verify held is not much different, in practice, from no boundary at all.

Sources

  1. modal.com
  2. modal.com
  3. cosmonic.com
  4. arxiv.org
  5. arxiv.org
  6. northflank.com

More in Sandbox Infrastructure