Security and Mediocrity: The Real Trade-offs of Agent Sandbox Wheels
When choosing a code sandbox for an agent, the main tension is between security and runtime compatibility. A complete standard library, common language features, and intuitive I/O behavior may look ordinary, but they are exactly what falls within the LLM's training distribution. The model assumes import os works, console.log produces output, and requests.get returns a result. Every time the sandbox removes a conventional capability, the prompt must compensate with extra explanation, and the cost of errors and retries must be borne.
This article refers to this compatibility as "ordinariness." It is not a pejorative term, but a measure of whether the LLM can write code based on its existing experience. Below we compare several implementations in the JavaScript and Python ecosystems, focusing on isolation models, public security track records, interruption and resource limits, common LLM errors, and state persistence.
How the big players choose
Let's first look at solutions already in production:
| Who | Scenario | Choice | Main trade-offs |
|---|---|---|---|
| OpenAI | Programmatic Tool Calling | Managed fresh V8, one per program | No Node, npm, console, network, or persistent state |
| OpenAI | Codex CLI (macOS) | Seatbelt (sandbox-exec) wraps tool calls |
Full runtime preserved, boundaries provided by the OS |
| Cloudflare | Code Mode | Dynamic Worker V8 isolate | Millisecond startup, no fetch, tools injected via pre-authorized bindings |
| Anthropic | Code execution with MCP | File system + TS files | Design provided; sandbox implementation left to users |
| Figma | Third-party plugins | QuickJS compiled to WASM | Accepts performance loss in exchange for structural isolation |
| Shopify | Functions | Javy (QuickJS→WASM) | Modules ≤256KB, execution ≤5ms |
| MetaMask | Snaps / supply chain | SES + LavaMoat | Frozen language environment; integrity only |
| E2B / Modal | General-purpose code execution | Firecracker / gVisor microVM | Native runtime preserved, boundaries provided by the kernel |
These solutions fall into two categories: restricting the runtime's capabilities, or preserving the full runtime and moving isolation to the process, OS, or VM. Attempts to enforce a strong security boundary inside the same full language runtime have a poor track record.
JavaScript
Problems with in-process Node isolation
vm2 used to be the most popular JavaScript sandbox on npm. It ran inside Node's own VM and used Proxies to restrict sandboxed code from accessing host objects. After more than twenty escapes, the maintainer announced discontinuation in 2023, citing critical security issues and saying it should no longer be used in production. The project briefly resumed maintenance in October 2025, then another CVSS 9.8 vulnerability appeared.
vm2 is built on node:vm, and Node's documentation explicitly states that node:vm is not a security mechanism and should not be used to run untrusted code. The problem is not just vm2's specific implementation; it is that Node has never designed same-process attacker code as a security boundary.
Figma's migration provides a direct comparison. It originally used a Realms shim to isolate plugins, reusing the browser's JavaScript engine with good performance and compatibility. But objects inside and outside the sandbox still lived in the same VM, and object identity confusion led to multiple escapes. After migrating to QuickJS-WASM, this class of vulnerabilities disappeared because objects have different representations on the two sides. Even though the Realms vulnerabilities were later fixed, Figma did not migrate back and continued to accept QuickJS's slower speed.
V8 isolates are not the final boundary
isolated-vm uses a real V8 Isolate to separate heaps and is the replacement recommended in vm2's README. Screeps, Fly.io, and Algolia have used it. Its README also warns that users without security experience can easily make serious mistakes. The project is currently in maintenance mode.
The critical escape disclosed in August 2026 exploited an ExternalCopy type confusion to achieve guest-to-host escape via a single Reference, affecting agent products such as n8n, Activepieces, Mastra, and Budibase. This also shows that an isolate cannot serve as the final security boundary on its own. V8's official data shows that over the past three years, six out of ten Chrome vulnerabilities exploited in the wild occurred in V8; Cloudflare also acknowledged in its Workers security model that isolates cannot stop Spectre. After a 12 bits/second cross-tenant leak demonstration in production appeared in August 2026, Cloudflare ultimately used process isolation and MPK as mitigations.
QuickJS-WASM and SES
quickjs-emscripten compiles QuickJS to WASM. Under compute-heavy loads, it is typically 10–50x slower than V8 and covers only part of the ES feature set; async requires asyncify and doubles the binary size. Cloudflare's judgment is that typical LLM scripts spend most of their time waiting on external tool calls, so interpreter performance differences have limited impact on these I/O-bound workloads.
QuickJS itself has still seen heap overflows, use-after-free, and other memory vulnerabilities. WASM linear memory serves to confine memory corruption within the boundary rather than eliminating interpreter bugs.
SES takes a language-level constraint approach: freezing intrinsics and injecting permissions via capabilities. MetaMask uses it to protect supply-chain risks for thirty million users. But SES only promises integrity; it does not handle unlimited CPU or memory consumption. Freezing prototypes also breaks some third-party libraries. It solves a narrower problem than a full sandbox.
Interruption and resource limits
The JavaScript side already has several interruption primitives: QuickJS's setInterruptHandler invokes a callback periodically during execution and can terminate while(true){}; V8 provides TerminateExecution; wasmtime provides epoch interruption and guarantees that the guest cannot bypass deadline checks. All three share the same limitation: when code is blocked inside a native call or host function, they can only wait for the call to return.
Resource limits cannot be judged merely by whether an API exists. In Simon Willison's comparison, Node's worker_threads with resourceLimits set to 4MB still used up to 88MB before termination; isolated-vm and QuickJS-WASM limits did take effect.
When runtime capabilities are removed, the common LLM errors are also stable: it defaults to console.log, fetch, or require, assumes npm exists, or writes async code in environments that only allow synchronous execution. Typically the capability list and injected function type signatures must be placed in the prompt, and raw errors are fed back to the model for correction.
Host APIs remain a significant attack surface. Check Point disclosed five memory corruption vulnerabilities in workerd at When Agentic Glue Melts (Black Hat 2026): the attack starts with prompt injection, then exploits a use-after-free in node:zlib to escape the sandbox and execute native code on the host. node:zlib was added as a Node API specifically to improve compatibility.
Python
Monty: a secure Python subset
Pydantic's Monty is a minimalist Python interpreter written in Rust with microsecond startup times. File system, network, and environment variables are only accessible through host-injected functions, making its isolation approach close to a Python version of QuickJS.
Its compatibility limitations are also obvious: it implements only a subset of Python, the standard library has only a handful of modules, and even classes were unsupported at release. Supporters argue that the LLM can rewrite code when it receives error messages; opponents argue that the model will spend reasoning effort on bypassing interpreter restrictions. The top HN criticism summarized it as "a safe version of eval() without the standard library." Pydantic publicly solicited the capabilities needed by LLMs, indicating that Monty is still filling in compatibility gaps.
Monty has already held three Hack Monty attack-defense competitions. Within 48 hours of the first round, attackers combined two GC bugs into a use-after-free escape, and the vulnerability was discovered by the attacker's AI agent; no one escaped in the next two rounds, and the bounty for the third round was raised to $20,000.
Other in-process approaches
Pyodide provides full CPython semantics and can run numpy, but the behaviors LLMs commonly use are incomplete: time.sleep is a no-op, requests is unavailable, the file system is virtual, and cold starts take seconds. langchain-sandbox used this route, but the repository is now marked as unmaintained and explicitly not recommended for production.
RustPython covers more language features than Monty, but its project RFC acknowledges it is currently unsuitable for secure embedding: infinite loops cannot be terminated and memory allocation has no upper bound. In practice, it is usually run as a WASI program with wasmtime providing resource limits. MicroPython has no visible use cases as an agent sandbox. The earlier pysandbox was abandoned by its author, and both RestrictedPython and audit hooks have known bypasses; CPython's introspection capabilities make complete in-process isolation very difficult.
Interruption and persistent state
Monty has three layers of passive limits: an interpreter cumulative time budget, a host-side hard timeout per round, and killing the child process after timeout. But it has no active cancellation API in the form of session.interrupt(). When a user stops execution, the entire session must be closed, and persistent REPL state is lost as well.
FastMCP once reported a related thread leak issue: after an asyncio task was cancelled, the sandbox's native thread still ran at full CPU until process exit. The issue was closed due to lack of a minimal reproduction. Monty's budget checkpoints already provide some of the machinery needed for interruption, but no active API has been exposed.
A full IPython experience requires a persistent namespace, Ctrl-C via OS signals, rich output and completion that rely on introspection, !pip install with C extension support, and a real file system. These capabilities are very difficult to preserve while using in-process isolation. Production solutions commonly choose kernel-in-a-box: instead of modifying IPython, they put the entire kernel inside a microVM or OS sandbox. OpenAI Code Interpreter, E2B (Firecracker, snapshot restore 5–30ms), and Modal (gVisor) all follow this structure, and interrupt_kernel() can directly send SIGINT.
Codex CLI is a lighter implementation: on macOS, a Seatbelt configuration wraps the full native toolchain, denying access by default and only opening writable directories and optional network. It isolates tool invocations, not the language runtime.
Why the two ecosystems differ
JavaScript's intermediate solutions were already mature before LLMs became widespread, driven by plugins and edge computing. Figma has run third-party plugins since 2019, Shopify needs to execute merchant code within 5 milliseconds, Cloudflare needs large-scale fast-starting isolated environments, and MetaMask needs to defend against supply-chain attacks. As a result, QuickJS-WASM, isolated-vm, and SES had accumulated production track records before becoming agent sandboxes.
Python only began to fill this gap in 2025 with the arrival of Monty, and at the time of writing V1 has not yet been released. Previously the main choices were Pyodide, or placing full Python inside processes, containers, and microVMs.
Language capabilities are not the main reason for the divergence. Research projects lean toward Python: CodeAct and smolagents use it to validate the advantages of code actions over JSON tool calls, citing language popularity, the package ecosystem, and evidence that LLMs are slightly better at writing Python. Production code modes lean toward JavaScript. From September to November 2025, Cloudflare, OpenAI, and Anthropic all used JS/TS; the existing V8, JSON-like literals, MCP's TS SDK, and the ability to generate typed interfaces from schemas all lowered implementation costs.
The pi ecosystem demonstrates both routes simultaneously. The Python extensions pi-codemcp and pi-code-tool are built on Monty and use snapshots to implement collaborative cancellation: freezing the script when a class-modifying tool call is made, waiting for approval, and even resuming from the original position days later. The JavaScript extension runline uses QuickJS-WASM to run agent code while keeping plugin implementations in Node outside the sandbox; pi-codemode offers two executors, using isolated-vm in Node environments and falling back to QuickJS-WASM when native modules cannot be installed.
Several common constraints
First, escapes often happen at the host connection layer. Monty's problems came from GC bugs in unsafe Rust, workerd's came from C++ bindings, isolated-vm's came from type confusion in object copying, and Realms' came from identity confusion between objects in the same VM. How many host APIs are exposed outside the interpreter deserves as much scrutiny as the interpreter itself.
Second, language-level restrictions cannot solve usability problems on their own. Infinite loops, memory exhaustion, and interactive interruption still require WASM epoch/fuel, or process and VM boundaries.
Third, a full REPL and strong in-process isolation are hard to have simultaneously. When native ecosystem, persistent state, and reliable interruption are needed, the recurring implementation is a full kernel plus an external sandbox.
How to choose
- Orchestration-style code mode: The LLM writes only dozens of lines of code to chain tool calls; programs are short, stateless, and mostly wait on I/O. For JavaScript, QuickJS-WASM is an option; if you choose isolated-vm, add an external security boundary. For Python, Monty is an option, but accept the language subset and the lack of active interruption.
- Data analysis and long sessions: When you need the full package ecosystem, persistent state, and IPython interaction, use kernel-in-a-box. Choose the isolation strength based on your threat model: Seatbelt/bubblewrap, containers, or microVMs.
- All solutions: Include interruption and resource limits in acceptance testing and verify that the limits actually work; review all host APIs; add a process or VM boundary on top of any language-level sandbox.
LLMs are most familiar with standard runtime behavior, so compatibility directly affects the reliability of generated code. Strong security does not have to be fully provided inside the language runtime. The more common approach in existing solutions is to preserve the language capabilities you need and then establish a boundary with WASM, a process, an operating system, or a VM.