← all writing

Sandboxing for AI Agents: Letting Models Run Code Without Losing Control

Sandboxing for AI Agents: Letting Models Run Code Without Losing Control

The last-mile problem for agents that can act

Every agent that gets asked to analyze a spreadsheet, clean a dataset, generate a chart, or verify its own arithmetic eventually runs into the same wall that tool-calling alone cannot get past: some tasks are only tractable if the model can write a small program and execute it, rather than trying to reason its way to the answer token by token.

Sandboxing is the answer to that danger, but the word hides a lot of different engineering decisions underneath it. A sandbox can mean a Unix process with a restricted set of system calls, a container sharing a kernel with its host, a microVM with its own virtualized hardware, or a WebAssembly module that never sees a real operating system at all. Each of these makes a different bet about how much isolation you need, how much it will cost you in latency and complexity, and what you are actually willing to assume about the code running inside. Getting this decision wrong in either direction is expensive: too little isolation and a single compromised or merely buggy agent can reach your infrastructure, too much and you have built a system so slow or so restrictive that the agent cannot do the job it was given.

Stacked shipping containers at a port

From tool calls to code as the universal tool

The earliest agent designs treated code execution as just another tool in the list: a function called run_python or execute_sql that the model could invoke with arguments, indistinguishable in shape from a function that looks up a weather forecast or searches a database. That framing undersells what is actually happening. A weather-lookup tool has a fixed, bounded set of behaviors baked into its implementation; a code-execution tool has an effectively unbounded set of behaviors, because the argument is not a small set of parameters but an entire program that can do anything the host language and its execution environment permit.

This distinction matters because it changes where the risk actually lives. With an ordinary tool, the attack surface is the tool's own implementation: if the weather tool has no bug, there is no way to misuse it beyond calling it with weird arguments. With a code-execution tool, the attack surface is everything the execution environment exposes, the filesystem, the network, the process table, whatever credentials happen to be reachable from that environment, because the code itself decides what to touch. A tool with a code-execution capability is not one tool among many; it is a gateway to every other capability the machine running it happens to have, and treating it with the same casualness as a bounded, single-purpose tool is where a lot of avoidable incidents start.

Agents reach for code execution constantly once you give them the option, and not only for the obvious cases like running a script the user explicitly asked for. Complex arithmetic, string manipulation, date logic, and data transformation are all things language models perform unreliably when done purely through generated tokens, but reliably when the model instead writes three lines of code and reads back the result. This pattern, sometimes described as letting the model reach for a calculator instead of trying to be one, has become common enough that a growing share of agent frameworks default to giving models a code-execution tool even when the user's task was never explicitly about programming. That default is convenient and often genuinely improves output quality, but it also means code execution is quietly present in far more deployments than the phrase "coding agent" would suggest, including many where nobody explicitly decided to accept the risk.

A code-execution tool is not one tool among many. It is a gateway to everything the machine running it can reach.

What we actually mean by isolation

Before comparing specific techniques, it helps to be precise about what a sandbox is supposed to guarantee, because "isolation" gets used loosely to describe things with very different strength. A useful way to break it down is into four separate boundaries, and a sandbox worth trusting needs an explicit answer for each one rather than a vague assurance that it is "isolated."

The first boundary is the filesystem: can code running inside the sandbox read or write anything outside a directory it was explicitly given, and does that directory disappear cleanly when the task ends. The second is the network: can code inside the sandbox open outbound connections, and if so, to where, since a sandbox that stops filesystem tampering but permits arbitrary outbound network access has left the door open for data exfiltration and command-and-control regardless of how locked-down everything else is. The third is process and kernel exposure: does the code share a kernel with the host, and if it does, does it have access to the full surface of system calls the kernel exposes, or a deliberately narrowed one. The fourth is resource consumption: can code inside the sandbox exhaust CPU, memory, disk, or wall-clock time in a way that degrades or crashes anything outside its own boundary.

A sandbox that handles three of these well and ignores the fourth is not mostly safe; it is exposed exactly along the dimension it ignores, and that is usually the dimension an attacker or a buggy generated script finds first, because it is often the one implementers assumed somebody else was handling. Evaluating a sandboxing technology means asking what it guarantees on all four axes, not just the one it was originally designed to solve.

The threat model, stated plainly

It is worth being specific about what a sandbox for agent-generated code is actually defending against, because the answer is broader than "malicious code," and treating it as narrowly as that leads to underinvesting in the parts of the threat model that do not look like a movie-plot hacker.

The most straightforward threat is code that is simply wrong: a generated script with an infinite loop, a runaway recursive function, an accidental fork bomb, or a typo that deletes more files than intended. This is not malice, it is the ordinary failure mode of any code, generated or handwritten, and a sandbox needs to contain it regardless of intent. The second threat is prompt injection reaching the code-generation step: content the agent read from a file, a webpage, or a tool result can contain text crafted to influence what code the model writes next, steering it toward exfiltrating data, deleting evidence, or reaching out to an attacker-controlled endpoint, all while looking like ordinary generated code to a human skimming it. The third threat is a genuinely adversarial user deliberately trying to use the agent's code-execution capability as a foothold into infrastructure it was never supposed to reach, testing the sandbox's boundaries the way any attacker would test any perimeter. The fourth, and the one that gets the least attention, is supply-chain risk introduced by the generated code itself: a script that innocently imports a popular package is still pulling in code nobody reviewed, and a sandbox that permits arbitrary package installation from within a task has widened its own attack surface without anyone deciding to do so on purpose.

A sandbox designed only around the first threat, accidental bugs, will look adequate in testing and fail against the second and third, because injected instructions and deliberate attackers do not announce themselves and do not look different from legitimate generated code until the moment the sandbox boundary is tested. Design for the adversarial case even when most of your traffic is not adversarial, because the cost of being wrong is not evenly distributed across the traffic you see.

The oldest tools: processes, chroot, and seccomp

Long before anyone was sandboxing AI-generated code, operating systems needed ways to run less-trusted programs with restricted privileges, and the resulting toolkit is still the foundation everything newer is built on. A chroot changes the root directory a process sees, so that from inside the process's point of view, a subdirectory of the real filesystem looks like the entire filesystem. It is cheap, has existed since the early days of Unix, and is trivial to escape if the process retains enough privilege to break out of the changed root, which is why it was never intended as a security boundary on its own so much as a convenience for build systems and testing.

Resource limits, set through rlimit and similar mechanisms, cap how much CPU time, memory, open file descriptors, or processes a given process tree can consume, which addresses the resource-exhaustion piece of the threat model directly and cheaply. Seccomp-bpf goes further and restricts which system calls a process is even allowed to make, using a filter that can allow a narrow whitelist, like read, write, and a handful of memory operations, while blocking everything else, including the system calls an attacker would need to open new files outside the chroot, spawn new processes, or manipulate other processes on the machine.

These primitives are not obsolete; they are the load-bearing mechanism underneath nearly every higher-level sandbox described below, including containers, which are built out of exactly these Linux kernel features rather than some separate, more exotic isolation technology. Understanding them matters even if you never configure a seccomp filter directly yourself, because it tells you what a container is actually promising and where that promise runs out.

A rack of physical servers in a data center

Containers: convenient, but not a security boundary by default

Containers, in the Docker or more general OCI sense, are the default reach for most teams building a code-execution tool, and for good reason: the tooling is mature, the developer experience is familiar, and spinning up an isolated environment for a task takes a single command. A container achieves its isolation through Linux namespaces, which give a process tree its own view of processes, network interfaces, mount points, and user IDs, combined with cgroups, which enforce the resource limits described above at a group level rather than a single process at a time.

The trap that catches teams new to this space is treating a container as though it were a security boundary equivalent to a full virtual machine, when the design intent was originally closer to convenient packaging and resource accounting than to hard security isolation. A container shares the host's kernel with every other container on that host and with the host itself. If there is an exploitable vulnerability in the kernel, reachable through a system call a container's default seccomp profile still permits, a process inside that container can potentially escalate out of it and onto the host, something that has happened in practice through documented vulnerabilities affecting mainstream container runtimes over the years, not a hypothetical concern invented to sell you a fancier alternative.

This does not mean containers are unsafe for running agent-generated code; it means they need to be configured deliberately rather than used with defaults designed for trusted, first-party workloads. A default container image typically ships with a broad seccomp profile, retains capabilities the sandboxed code has no legitimate need for, and runs as root inside the container unless explicitly told not to. Hardening a container for untrusted code execution generally means dropping every Linux capability that is not strictly required, running as an unprivileged user, applying a restrictive seccomp profile rather than the runtime's default one, mounting the filesystem read-only except for an explicitly designated scratch directory, and disabling networking unless the task genuinely needs it. Every one of those steps is available in standard container tooling; the risk is not that containers cannot be made reasonably safe, it is that the defaults quietly assume a different, more trusting threat model than the one you actually have when the code being run was written by a language model responding to untrusted input.

A container's default configuration assumes a trusted workload. Code generated by a model responding to untrusted input is not that.

MicroVMs: paying for a stronger boundary

For workloads where the isolation containers provide is not enough, the next step up is virtualization: giving the sandboxed code its own kernel, running inside a hypervisor, rather than sharing the host's kernel through namespaces. A full traditional virtual machine achieves this but at a real cost in boot time and resource overhead that makes it a poor fit for spinning up a short-lived sandbox per agent task, sometimes many times a minute.

MicroVMs were built specifically to close that gap. Firecracker, developed at Amazon originally for running Lambda functions, strips a virtual machine down to the minimal device model an untrusted workload actually needs, and can boot a new, fully isolated virtual machine in on the order of tens of milliseconds rather than the seconds a general-purpose VM typically takes. That speed matters enormously for agent workloads specifically, because an agent that needs to spin up a fresh, fully isolated environment for every code-execution call cannot tolerate multi-second startup latency without the sandboxing itself becoming the dominant cost of the interaction.

The security property a microVM buys you, compared to a container, is that a kernel exploit inside the sandboxed workload only compromises the guest kernel running inside that specific microVM, not the host kernel shared across every workload on the machine. This is a materially stronger isolation guarantee, because it removes an entire, historically productive category of escape, kernel vulnerabilities, from the attacker's reachable surface, replacing it with the hypervisor's own attack surface, which is smaller, more scrutinized, and has a much better track record specifically because hypervisor isolation has been the security boundary underpinning multi-tenant cloud computing for over a decade.

An alternative approach to the same problem is gVisor, which does not virtualize hardware but instead interposes a user-space kernel between the sandboxed process and the real one, intercepting system calls and implementing a large portion of the Linux kernel interface itself rather than passing calls through to the host kernel directly. This shrinks the amount of real host kernel surface a sandboxed process can reach, at the cost of reimplementing kernel behavior in user space, which introduces its own compatibility quirks and a performance overhead that varies significantly depending on how system-call-heavy the workload is. Both approaches, hardware virtualization and user-space kernel interposition, are legitimate answers to the same underlying question, and the right choice between them typically comes down to whether your workload's performance profile tolerates gVisor's per-syscall overhead better than it tolerates a microVM's memory footprint per instance.

WebAssembly: sandboxing by construction rather than by containment

A different strategy altogether is to avoid giving the code a real operating system to interact with in the first place. WebAssembly, originally designed to run untrusted code safely inside web browsers, achieves isolation not by containing a process after the fact but by construction: a WebAssembly module has no ambient access to the filesystem, network, or any other system resource unless the host explicitly grants it through an imported function, following a capability-based security model where the absence of a capability is the default rather than something that has to be actively revoked.

This inversion, deny by default rather than allow by default and then subtract, is a meaningfully different security posture than every technique described so far. A container or a microVM starts from an operating system's full capability set and removes pieces through namespaces, capabilities, and seccomp filters, which means the safety of the result depends entirely on the completeness of what got removed. A WebAssembly module starts from nothing and only gains the specific capabilities the host explicitly wires up, which means a misconfiguration tends to fail by the sandboxed code being unable to do something it needed, a functional bug that is loud and immediately visible, rather than by the sandboxed code being able to do something it should not have, a security bug that is silent until exploited.

WASM sandboxes also tend to be extremely lightweight and fast to instantiate, often faster than even a microVM, and are deterministic and portable across host architectures in a way containers are not, which makes them attractive specifically for the high-frequency, short-lived code execution pattern common in agent tool use, where a task might involve dozens of small code-execution calls rather than one long-running session. The tradeoff is ecosystem maturity: compiling an arbitrary Python or Node.js script to WebAssembly and getting full standard-library compatibility is still a work in progress for a lot of common libraries, particularly ones with native extensions, whereas containers can run essentially anything you can install. For agent workloads centered on structured data manipulation, calculation, or well-supported languages with mature WASM toolchains, this tradeoff often favors WASM; for workloads that need arbitrary package ecosystems with native dependencies, it currently does not.

Language-level sandboxes and why they keep failing

A tempting shortcut, especially early in a project, is to sandbox at the language level instead of the operating-system level: restrict a Python interpreter's builtins, blocklist dangerous modules like os and subprocess, and let the code run in the same process as everything else. This is attractive because it is cheap to build and adds no infrastructure, and it is also, with very rare exceptions, not a real security boundary, a lesson the Python community in particular has learned repeatedly and painfully enough that most serious guidance now actively warns against it.

The problem is that a general-purpose, dynamically typed language gives an attacker an enormous number of ways to reach dangerous functionality indirectly even after the obvious direct paths are blocked. Python's object model in particular exposes so much introspection, the ability to walk from any object to its class, to its base classes, to other objects reachable through attributes, that a restricted-builtins sandbox has historically been defeated by chaining together seemingly harmless operations to eventually reach a reference to an unrestricted builtin or an unrestricted function, entirely without calling anything on an explicit blocklist. This is not a one-off vulnerability that got patched; it is closer to a structural property of the language, and every well-known attempt at a pure-Python sandbox, including ones built by very good engineers, has eventually been broken by exactly this pattern.

The honest conclusion is that language-level restriction is a reasonable second layer of defense, worth having because it narrows the practical attack surface and slows down casual misuse, but it is not a substitute for a real process or kernel-level isolation boundary underneath it. If the only thing standing between generated code and your host filesystem is a Python function that checks whether the code called a dangerous function directly, you do not have a sandbox, you have a speed bump, and the speed bump should sit inside a container or microVM, not instead of one.

Network egress: the boundary everyone forgets

Of the four isolation axes described earlier, network egress is the one most commonly under-specified, and it is also the one where the consequences of getting it wrong are hardest to reverse. A sandbox that perfectly isolates the filesystem and caps CPU and memory but allows unrestricted outbound network access has not actually contained the code; it has only contained what the code can do to the machine it is running on, while leaving open every path for that code to exfiltrate whatever data it can read, reach internal services on the same network the sandbox host happens to sit on, or communicate with infrastructure controlled by whoever crafted an injected instruction.

The default posture worth adopting is no network access at all, with specific, named exceptions granted only when a task genuinely requires reaching a particular external service, and even then routed through an egress proxy that can log, rate-limit, and restrict which destinations are reachable rather than opened as a blanket allowance. This matters more for agent-generated code specifically than it would for a typical containerized microservice, because a microservice's network behavior is defined by code a human wrote and reviewed, while a sandbox running agent-generated code is running a program whose author was a model responding, possibly under the influence of injected content, to a task description at inference time. You cannot review that code the way you would review a pull request before every single execution, so the network boundary has to hold regardless of what the code decides to attempt, rather than relying on the code having been vetted in advance.

A sandbox that isolates the filesystem but not the network hasn't contained the code. It has only contained what the code can do to the machine, not what it can do with the data.

A padlock symbolizing security boundaries

Filesystem isolation and the case for ephemeral environments

The filesystem axis benefits from a design choice that is simple to state and easy to under-invest in: every code-execution environment should be disposable, created fresh for a task and destroyed completely when that task ends, rather than reused across tasks or across users. A persistent sandbox that gets reused accumulates state between executions, files left behind by a previous task, cached packages, environment variables, and that accumulated state is both a correctness hazard, since one task's leftovers can silently affect another's results, and a security hazard, since it creates a channel for one task's output to affect a later task's execution in ways nobody explicitly designed.

Modern container and microVM tooling makes ephemeral environments cheap enough that reuse is rarely justified purely on performance grounds anymore. Copy-on-write filesystem layers mean spinning up a fresh environment from a known-good base image costs very little beyond the first environment, since only the differences from the base need to be materialized, and snapshot-and-restore techniques in some microVM implementations can resume a pre-warmed environment from a checkpoint even faster than a cold boot, giving you both the safety of a fresh environment and much of the speed of a reused one. The discipline worth adopting is treating any environment that ran agent-generated code as contaminated the moment that code finished running, regardless of how the code behaved, and never trusting a clean-looking exit as evidence that nothing was left behind.

Within that ephemeral environment, the remaining filesystem question is how narrowly to scope what is writable at all. A task that only needs to read a provided dataset and produce a chart image needs a read-only mount for the input and a narrow, size-capped writable directory for the output, nothing more, and code that has no legitimate reason to write outside that directory should not be able to, regardless of what it attempts. Every additional writable path is a path that has to be individually justified, not one that is available by default and occasionally restricted.

Resource limits as a first-class design decision, not an afterthought

Denial-of-service protection tends to get treated as a secondary concern behind the more dramatic-sounding threats of data exfiltration or privilege escalation, but for agent-generated code specifically, resource exhaustion is often the most likely failure mode in practice, precisely because it does not require anything adversarial to happen. A generated script with an off-by-one error in a loop condition, an accidental quadratic algorithm applied to an unexpectedly large input, or a recursive function missing its base case will occur regularly in ordinary, well-intentioned use, and a sandbox that has not planned for it will either hang indefinitely or take down shared infrastructure with it.

The concrete controls here are not exotic: a hard wall-clock timeout on every execution, independent of whether the code is making progress, since a slow-but-technically-working script is often just as unacceptable as a hung one from the perspective of an agent loop waiting on the result. A memory ceiling enforced by the isolation layer itself rather than trusted to the code's own behavior, since out-of-memory conditions in a shared environment can affect neighboring workloads before the offending process is even killed. A cap on output size, since a script that generates gigabytes of print statements or writes an enormous file can exhaust disk or memory just as effectively as one with a runaway computation, and this is a surprisingly common failure mode that has nothing to do with malice, just generated code that entered an unexpected loop. And a limit on process and thread creation, since an unbounded fork loop is one of the oldest denial-of-service patterns in existence and one of the easiest to prevent with a resource limit set before the code ever runs, rather than detected after the fact.

None of these limits are individually sophisticated, but the discipline that matters is applying all of them by default, on every execution, rather than treating them as configuration to add reactively after the first incident. A sandbox that gates on some of these but not others has a threat model with a hole in it exactly where the missing control would have been, and that hole tends to be found by ordinary buggy generated code well before it is found by anyone trying to attack the system on purpose.

Where humans still belong in the loop

Isolation is necessary but it is not the only lever available, and treating sandboxing as the sole defense misses that some actions are dangerous enough, or hard enough to reverse, that the right control is not a stronger technical boundary but a human decision point inserted before the code runs at all, not just around what it can touch once it is running. A sandbox can perfectly contain a script that deletes every file in a customer's connected storage bucket; containment does not help if deleting those files was itself an authorized, intended action that the sandbox correctly allowed, because the problem was never that the code escaped its boundary, it was that the action itself needed a human to confirm it was actually wanted before it happened.

The practical pattern is to classify actions by reversibility and blast radius rather than treating every code execution identically. Code that reads data, performs a calculation, or produces an artifact for later review can generally run automatically within a well-configured sandbox, because a mistake is cheap to detect and cheap to undo. Code whose effect reaches outside the sandbox in a way that is hard to reverse, sending an email, modifying a production record, deleting stored data, initiating a payment, deserves an explicit confirmation step regardless of how well-contained the execution environment is, because the sandbox was never the layer meant to catch an unwanted-but-authorized action in the first place. This is the same distinction that shows up in broader guardrail design for agents generally, applied specifically to the code-execution surface: containment protects against what the code should not be able to do; human confirmation protects against consequential things the code is fully able to do but that nobody has actually approved yet.

Watching what happens inside the box

A sandbox that is opaque from the outside creates a second-order problem even when it holds perfectly: when something goes wrong, whether that is a script failing, a task taking longer than expected, or a security review needing to understand what a piece of generated code actually did, there needs to be a record detailed enough to reconstruct the execution without requiring access to the now-destroyed ephemeral environment itself.

The practical answer is to treat observability as something the isolation layer produces as a side effect, rather than something bolted onto the sandboxed code's own behavior, which cannot be trusted to log its own actions faithfully if it is exactly the thing you do not fully trust. Capturing the full command or script that was executed, the exit code, resource consumption over the execution's lifetime, and any network connection attempts, including ones that were blocked by the egress policy rather than only ones that succeeded, gives you a record that exists independent of whatever the code inside the sandbox chose to report about itself. That last detail matters more than it might first appear: a blocked network call is not a non-event to skip logging, it is often the single most informative signal available that something unexpected, whether a bug or an injection attempt, was attempted during that execution, and a logging setup that only records successful actions will systematically miss the attempts most worth investigating.

This kind of execution-level logging is a natural extension of the broader observability discipline that any production agent system needs regardless of whether code execution is involved, but it deserves specific attention at the sandbox boundary because the alternative, relying on the sandboxed code to self-report faithfully, quietly assumes away exactly the failure mode a security-conscious design is supposed to account for.

A terminal command line interface

A worked example: an agent that analyzes a spreadsheet

It helps to walk through what good practice actually looks like assembled together, rather than staying at the level of individual controls. Consider an agent given a task: read an uploaded spreadsheet, compute some summary statistics, and produce a chart. The agent decides the cleanest way to do this is to write a short Python script using a data analysis library and execute it, rather than trying to compute aggregate statistics through generated text alone, which would be both slower and considerably less reliable.

A well-built system spins up a fresh, ephemeral microVM or hardened container for this specific call, mounts the uploaded spreadsheet read-only at a known path, and mounts a separate, size-capped, writable scratch directory for any output the script produces. Network access is disabled entirely for this execution, since nothing about summarizing a local file requires reaching the internet, and if the generated script attempts an outbound connection anyway, whether because of a stray import that tries to phone home or because injected content in a cell somewhere in that spreadsheet steered the model toward writing code that reaches out to an external address, that attempt is blocked at the network layer and logged rather than silently allowed or silently failing in a way nobody notices. A wall-clock timeout and a memory ceiling are applied regardless of how simple the task looks, because simple-looking generated code produces runaway loops often enough that skipping this step just this once is exactly how the exception becomes the incident. The script runs, its full text, exit code, and resource usage are logged, and the resulting chart file is picked up from the scratch directory, at which point the entire environment is destroyed rather than kept around for the next task.

None of this requires the agent framework itself to know anything about sandboxing, this is entirely infrastructure sitting underneath the tool-calling interface, invisible to the model, which still just sees a code-execution tool that takes a script and returns a result. That invisibility is the point: the model's job is to decide what code to write, and the sandbox's job is to guarantee that decision cannot cause harm disproportionate to what the task actually needed, and the two responsibilities should stay cleanly separated rather than the model needing any awareness of, or trust placed in, its own containment.

How sandboxes actually get escaped

It is worth studying real escape techniques rather than treating sandbox failures as an abstract possibility, because the recurring patterns are more instructive than any individual vulnerability. Symlink and path-traversal attacks exploit a sandbox that checks a file path for safety once and then operates on it later, allowing code to swap what a path points to between the check and the actual file operation, a category of bug general enough to have its own name, time-of-check to time-of-use, and general enough that it shows up well outside sandboxing entirely. Kernel exploits, the primary concern that motivates microVMs over plain containers, take advantage of the fact that a container shares a kernel with its host, so any exploitable bug in a system call the container's seccomp profile still permits is a potential path all the way out to the host, a risk category that essentially does not exist for a properly configured microVM because there is no shared kernel to exploit in the first place.

Side channels are a subtler and less commonly discussed category, where code inside a sandbox cannot directly read data outside its boundary but can infer something about it indirectly, through timing differences, cache behavior, or resource contention shared with other workloads on the same physical hardware. These attacks are harder to execute and harder to defend against completely, and they are the reason genuinely high-sensitivity workloads sometimes go a step further than a microVM and insist on dedicated hardware rather than any form of multi-tenant sharing, even virtualized. And simple misconfiguration, a container run with more Linux capabilities than it needed, a network policy that was supposed to block everything but had one exception left open from testing, a resource limit that was set but not actually enforced because of how the runtime applied it, accounts for a large share of real-world sandbox failures, arguably a larger share than any of the more exotic techniques above, because a sandbox with a strong theoretical design and a sloppy default configuration is exactly as exposed as a weak design, just with a longer paper trail explaining why nobody expected it to be.

The lesson across all of these is consistent: a sandbox is not a single control that is either present or absent, it is a set of independent boundaries that each need to hold, and the practical failures cluster overwhelmingly around whichever boundary got the least explicit attention during setup, which is rarely the same boundary as the one the team spent the most time worrying about in the abstract.

Choosing the right sandbox for your situation

With this many options on the table, the decision worth making explicit is not "which sandboxing technology is best" in the abstract, but which one fits the specific shape of your workload, and that shape is defined mostly by three questions. How often does code execution happen, and how latency-sensitive is the task waiting on it: a workflow running a handful of executions per user session tolerates a slower, heavier isolation boundary far better than one running dozens of short code-execution calls per second, where instantiation overhead becomes the dominant cost. How trusted is the input that led to this code being generated: code generated in response to a task description a user typed directly is meaningfully lower risk than code generated in a pipeline that also ingests documents, web pages, or other content from sources nobody vetted, since that ingested content is exactly where prompt injection targeting the code-generation step tends to originate. And how sensitive is what the sandbox sits next to: a sandbox running on infrastructure with no access to production credentials or customer data warrants a different, lighter posture than one running inside the same network as systems that do.

For low-frequency, exploratory workloads where user trust is reasonably high and the surrounding infrastructure is not especially sensitive, a well-hardened container is often entirely sufficient, provided the hardening described earlier, dropped capabilities, restrictive seccomp, no default network access, ephemeral by design, is actually applied rather than left at runtime defaults. For high-frequency, latency-sensitive workloads or any workload processing content from untrusted or unvetted sources, the additional isolation of a microVM or a WebAssembly sandbox earns its cost, and the choice between those two generally comes down to whether your workload's language and library needs are well-served by current WASM toolchains or need the fuller compatibility a microVM's real Linux kernel provides. And for anything sitting adjacent to genuinely sensitive infrastructure, regardless of frequency, the additional cost of the strongest available isolation is close to a rounding error next to the cost of a real breach, which is the calculation that should end the debate in favor of paying for it.

The case against over-engineering this

None of this is an argument that every code-execution feature needs a microVM fleet and a dedicated egress proxy from day one. A weekend project letting a single trusted developer run small scripts against their own local data, with no untrusted input anywhere in the pipeline and no sensitive infrastructure nearby, gets real but limited value from investing heavily in sandbox infrastructure before it has any actual users, and the engineering time is often better spent elsewhere at that stage.

The judgment call is the same one that applies to most security investment: match the isolation to the actual threat model rather than the maximum available isolation, and revisit that judgment explicitly the moment any of its underlying assumptions change, moving from a single trusted developer to arbitrary users, from fully-authored input to content ingested from external sources, from an isolated test environment to infrastructure that sits near anything sensitive. The mistake is not choosing a lighter sandbox for a genuinely low-risk situation; the mistake is choosing a lighter sandbox and then never revisiting that choice as the situation around it changes, quietly carrying an early-stage risk decision into a production system that has long since outgrown the assumptions it was originally made under.

Where this is heading

The direction of travel in this space points toward isolation that is faster and more automatic without disappearing entirely, which is a healthy trajectory rather than a concerning one. MicroVM startup times have already dropped enough that the performance argument for weaker isolation is eroding steadily, and it is a reasonable bet that within a few years, the performance gap between a hardened container and a microVM will be small enough that microVMs become the unremarkable default for any code-execution tool, the same way containers became the unremarkable default for ordinary deployment over the preceding decade, quietly moving from a specialized choice to an assumed one. WebAssembly's ecosystem gaps, particularly around native library compatibility, are also closing, and a capability-based, deny-by-default model is a genuinely better security posture for an untrusted-code problem than a remove-permissions-after-the-fact one, which suggests it will keep gaining ground for the workloads its compatibility story can already support.

The area still most unsettled is not the isolation technology itself but the layer immediately around it: standardized, portable ways to describe what a given code-execution environment is allowed to touch, in a form that travels with an agent framework rather than being reimplemented bespoke by every team that builds one. Something like a widely adopted manifest format for sandbox policy, this environment gets no network, this filesystem path, this timeout, would do for sandbox configuration roughly what the Model Context Protocol did for tool connectivity: turn a decision every team currently makes bespoke, and inconsistently, into a shared, portable, well-scrutinized default that most teams simply adopt rather than reinvent, with the security benefit that comes from many eyes reviewing one shared implementation instead of few eyes reviewing many divergent ones. That piece of infrastructure does not fully exist yet in a form the ecosystem has converged on, and it is worth watching for, because it would meaningfully lower the odds that a given team's sandbox has a hole in exactly the boundary nobody thought to check.

Getting started without overbuilding it

For a team adding code execution to an agent today, the pragmatic path starts narrower than the full survey above might suggest. Begin with a hardened container, drop every Linux capability the workload does not explicitly need, disable networking by default and add narrow, logged exceptions only where a task genuinely requires them, make every environment ephemeral from the first version rather than something to add later, and apply a wall-clock timeout and a memory ceiling before the first line of untrusted code ever runs. That baseline, done properly rather than left at defaults, is already meaningfully safer than what a large share of shipped code-execution tools actually implement, and it does not require adopting a microVM fleet or a WebAssembly runtime to get there.

Move to stronger isolation, a microVM or a WASM-based sandbox, when the frequency of execution, the untrustedness of the input feeding the code-generation step, or the sensitivity of nearby infrastructure actually justifies the added cost, and revisit that decision explicitly whenever any of those three factors changes rather than assuming an earlier decision still holds. Log everything the isolation layer can tell you about an execution, including the attempts it blocked, not only the actions it allowed, since the blocked attempts are frequently the most informative signal you will get about whether something has gone wrong. And keep a human in the loop for any action a piece of generated code might take that would be expensive or impossible to undo, because that protection was never the sandbox's job to provide in the first place, no matter how well the sandbox itself holds.

Code execution is one of the most useful capabilities you can give an agent, and also one of the few where getting the surrounding infrastructure wrong turns a single bad decision by a language model into a real incident on real infrastructure rather than a wrong answer in a chat window. The technology to contain that risk well already exists and is more accessible than it was even a few years ago; what it requires from a team building on top of it is not exotic expertise so much as the discipline to apply the boundaries that matter by default, on every execution, rather than as a response to the first time one of them turns out to have been missing.