AI Engineer Europe 2026
Why, and how you need to sandbox AI-Generated Code? — Harshil Agrawal, Cloudflare
Read the talk
Why and how you need to sandbox AI-generated code?
Generated code runs with whatever privileges you give it. Two applications show how to choose an execution boundary, restrict access, and manage the sandbox’s lifetime.
From a talk by Harshil Agrawal
Before you start: Familiarity with JavaScript or TypeScript, environment variables, HTTP APIs, and basic container concepts will help with the implementation discussion.
Would you run this snippet with production credentials?
Have you built something where an LLM generates code that actually runs? The path from autocomplete to autonomous execution is short: suggest a line, generate a module, select a tool, then write, execute, review, and revise code without asking at each step. Each transition gives the system more responsibility for what happens inside your application.
Now remove the AI framing. A service receives code from a black box, does not necessarily review every line, and executes it with application credentials. Finding a snippet on a random website and calling eval on it in production would raise an immediate security objection. Generating the snippet with an LLM does not remove that objection. AI-generated code is untrusted code. It can be correct, subtly broken, excessively helpful, or shaped by adversarial input; none of those outcomes requires the model to have malicious intent.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Three routes from generated code to damage
The first threat exists even without attackers. A model imports a nonexistent package, writes recursion without a base case, or misunderstands a termination condition and produces an infinite loop. The resulting code can crash a process, exhaust its stack, or consume compute indefinitely. Good intentions do not contain the failure.
The second threat looks like reasonable initiative. Asked to configure a database connection, generated code inspects the environment to discover available configuration. In doing so, it reads API keys, database credentials, and other secrets. It may never attempt to steal them, but sensitive data has already entered code you did not audit. The apparent usefulness of that behavior makes it easy to overlook.
The third threat is prompt injection. A direct attack asks the model to ignore its instructions and generate code that sends environment variables to an attacker’s destination. An indirect attack places instructions inside a webpage or document the model reads while carrying out an otherwise innocent request. The model becomes an attack vector through its normal consumption of task data.
All three become dangerous for the same architectural reason: code executing inside the application can inherit the application’s filesystem, environment, network, database access, and API keys. A hallucination can become an outage, helpful exploration can become secret exposure, and injected instructions can become exfiltration because the generated code received production privileges.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Give the code only the keys it needs
Sandboxing already underpins familiar systems. Browsers separate web content using origin and site boundaries, permissions, and process isolation; the tab analogy is useful, though tabs are not universally independent cookie or DOM security boundaries. Operating systems isolate processes so an application failure need not bring down the machine, although that protection is not absolute. Phones keep application data separate and require permissions for capabilities such as the camera, contacts, and microphone.
The transferable principle is capability-based security: start with no authority, then explicitly grant the minimum required. Harshil Agrawal compares two ways of controlling access to a building. You could hand someone a master key and a list of ten thousand forbidden rooms, or give them keys to the three rooms they need.
| Approach | What you must specify | Failure mode |
|---|---|---|
| Blocklist | Every forbidden operation | An omitted operation remains available |
| Explicit capabilities | Only permitted operations | Ungranted operations are unavailable |
A blocklist requires anticipating dangerous system calls and APIs. An explicit capability interface makes the allowed surface small enough to inspect: a particular query method, a particular service, or a particular permission. The same reasoning that keeps a webpage from using your camera without permission should govern generated code.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Choose a boundary, then inspect five surfaces
eval supplies no isolation boundary: generated code executes inside the calling process with access available in that context. Isolates provide separate JavaScript execution environments with a deliberately constrained runtime. Containers supply a Linux environment with files, processes, and networking, enabling repository clones, package installation, and development servers.
Agrawal characterizes isolate startup as about one millisecond and container startup as a few seconds. These are approximate descriptions, not measurements of the applications demonstrated here; Cloudflare’s Dynamic Workers announcement uses different approximate startup figures. The useful distinction is the workload each environment supports, rather than a promised latency ratio.
Before selecting a tool, give definite answers to five questions:
- Secrets: Can generated code read environment variables, API keys, or database credentials?
- Networking: Can it make outbound requests, reach internal services, or send data away over HTTP?
- Filesystem: Can it read outside its workspace, including configuration, application code, or other users’ files?
- Tenants: Can one user’s code see another user’s data or interfere with another execution?
- Resources: Can it loop forever or allocate unbounded memory?
The last question concerns availability as well as cost. A runaway computation can exhaust a budget and deny service to legitimate requests. These boundaries need explicit answers before execution begins.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
A generated skill and a generated application
Two applications make the choice concrete. The first generates small, repetitive functions that need selected platform APIs and nothing else. Agrawal sets a sub-millisecond speed requirement for these functions; he does not report a measured result. He chooses V8 isolates. The second generates motion graphics from natural-language requests and needs dependencies, a development server, and a live preview URL. That requires a filesystem, package manager, and processes, so he chooses containers.
The first recorded demo is a Cloudflare-based alternative to OpenClaw. OpenClaw can generate its own skills using filesystem and internet access. Agrawal’s agent has some filesystem access but cannot run shell commands, so he gives it a narrower capability: write JavaScript and execute it on demand. Asked to create a skill that fetches top Hacker News stories, the agent reasons about the task, calls a tool to generate the skill, and executes the resulting code in an isolate.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Load a module with a narrow capability surface
The parent application uses Dynamic Workers to create Worker isolates at runtime. The generated module has its own memory, execution context, and global scope. It cannot reach into the parent Worker’s memory or obtain its environment variables simply because the parent created it. The application supplies the capabilities it needs: perhaps a restricted database interface and a logger.
The walkthrough’s core operation is loader.load: load the generated code as a module, define its bindings, then invoke it as a Worker. The important network setting is globalOutbound: null. It denies direct outbound networking while leaving explicitly granted bindings usable; it does not make those bindings or the returned response incapable of carrying data. The following TypeScript builds the capability portion of that loader configuration:
typescript
function skillCapabilities<DatabaseBinding, LoggerBinding>(
database: DatabaseBinding,
logger: LoggerBinding,
) {
return {
globalOutbound: null,
env: { database, logger },
};
}
The caller supplies a query-only, user-scoped database binding and a logger—not the parent’s complete environment. It then sends a request to the loaded Worker and receives a response. The security work lies in constructing those narrow bindings and withholding everything else.
Inside the generated code, database.query calls an RPC stub. That call returns to trusted Worker code, where the application controls which methods exist and which arguments are valid. The database binding is scoped to the current user, so it does not expose another user’s records. Parent secrets are absent because they were never passed in. This replaces broad ambient access with a small interface whose behavior the application owns.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Make networking an explicit decision
Network access has three useful policy levels:
| Policy | Behavior | Appropriate use |
|---|---|---|
| Blocked | globalOutbound: null | Code needs no direct network access |
| Mediated | Requests pass through an owned service | Approved APIs or webhooks |
| Unrestricted | Code chooses its destinations | Avoid for untrusted code |
Start with blocked access. When a skill genuinely needs an API call, route requests through a service you control. That service can allow specific domains, record requests, add authentication headers, and enforce rate limits. This supplies useful access without handing the generated module arbitrary egress. A version you trust today is insufficient justification for unrestricted access when the code can change tomorrow.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Where isolate constraints fit
The discussed runtime supports JavaScript, TypeScript, Python, and WebAssembly, but not arbitrary native executables. The distinction is the executable format and available runtime capabilities: supported Rust, Go, or C code can be compiled to WebAssembly, without gaining a Linux process environment. Similarly, the walkthrough’s lack of disk access should not be read as a claim that Workers have no file APIs: current Workers can expose an in-memory virtual filesystem. That still does not provide the Linux package-installation and process workflow needed by the second application.
Persist data through a database, Durable Object, or KV binding. Agrawal describes the intended functions as stateless and their invocations as fresh contexts; the reliable application contract is to externalize required state, rather than assume every request necessarily receives a newly created isolate. CPU and memory limits also bound what a generated function can do. These restrictions fit short functions, tool calls, plugins, skills, data transformations, and constrained agent code interpreters. They are less suitable for heavy computation or tasks that require a full operating system.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
A motion preview needs more than a function
The second demo begins with a request to animate a logo. More specifically, the user asks for a highlight on the supplied logo. The agent evaluates the request, writes code, starts a development server, and displays a generated video preview. The visible result contains the text “Generated with images-ai.app” and a yellow highlight.
Agrawal identifies the application as PromptMotion, which he describes as live in production. Supporting its preview workflow means cloning a starter repository, installing npm dependencies, running a build, starting a development server, and exposing a port—while keeping simultaneous users isolated.
| Requirement | Needed capability |
|---|---|
| Clone the starter repository | Git and writable files |
| Install dependencies | Package manager and processes |
| Run the development server | Long-lived process and listening port |
| Return a preview URL | Reachable route to the server |
These requirements exceed the constrained execution environment used for the generated skill. The missing piece is a full Linux environment, which is why this workflow moves to a container.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
From a user identifier to a preview URL
Each user receives a separate container with separate files and processes. If user A’s script lists its workspace, it sees user A’s files. User B’s files are not merely hidden by an application-level check; they are absent from that container’s filesystem.
The orchestration path is Worker → Sandbox SDK → Durable Object → container. The Sandbox SDK gives the application an interface to the environment. A Durable Object coordinates the sandbox’s lifecycle, and the container supplies Linux files, processes, and controlled networking. Inside it, the configured tools can include Bash, Node.js, Git, and npm.
The implementation walkthrough follows this sequence:
- Select the user’s sandbox. The user identifier determines which isolated environment receives the work.
- Clone the starter repository inside it. Git writes into the container’s filesystem.
- Install dependencies inside it. The parent Worker does not install or execute the generated application’s packages in its own environment.
- Start the development server in the background. The process remains alive to serve the preview.
- Expose its port and return the URL. The user can visit the running application.
Files, packages, and processes all remain on the container side of the boundary.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Separate users and keep secrets outside
One user, one sandbox is the tenant rule in this design. Sharing a sandbox means sharing a filesystem, which creates a path to another user’s generated code, data, or secrets. Calling the applications demos does not remove that exposure. It is also an architectural choice that becomes difficult to reverse once the rest of the system depends on shared execution.
A separate container still should not receive the application’s API keys. Agrawal admits having used the tempting shortcut of passing a key as an environment variable when generated code needed an external data source during a build. Once injected, that key is readable by code inside the container—including prompt-influenced code and buggy code that logs its environment.
Instead, give the sandbox access to a trusted proxy operation. The sandbox calls a Worker endpoint; the Worker adds the real authentication header, forwards the request to the external service, and returns the response. The API key stays in the Worker’s environment throughout. This separates permission to perform an operation from possession of the credential that authorizes it.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Make cleanup part of the operation
A container’s useful lifetime must have an end. Depending on the application, that might be completion of a build, closure of a preview session, or a session timeout. Use try/finally so ordinary failures and thrown exceptions still trigger destruction. A catch block alone is not a cleanup guarantee. For a bounded job, the pattern can be expressed directly in TypeScript:
typescript
async function withSandbox<
S extends { destroy(): Promise<unknown> },
Result,
>(
create: () => Promise<S>,
run: (sandbox: S) => Promise<Result>,
): Promise<Result> {
const sandbox = await create();
try {
return await run(sandbox);
} finally {
await sandbox.destroy();
}
}
For an interactive preview, the managed lifetime must include the viewing session; destroying the container immediately after returning its URL would remove the server the user needs.
Idle environments retain generated code and potentially cached user data while remaining a cost and security surface. Agrawal suggests reconsidering a sandbox after thirty minutes without interaction. He also mentions a ten-minute default timeout; in current Sandbox SDK terminology, that value is an inactivity sleep setting, not a universal hard maximum lifetime. Sleep and explicit destruction serve different purposes, so set a maximum lifetime and a cleanup policy deliberately.
Containers carry costs beyond startup: allocated CPU and memory per sandbox, plus SDK, Durable Object, orchestration, and networking components to operate. Agrawal rejects their startup overhead for a plugin that must respond with sub-millisecond latency on every API request. But files, package installs, and long-running servers justify that overhead when the application needs them. Trying to force those requirements into an isolate creates a more fragile design.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Choose the environment for the current step
The decision rule is practical: does this step need a Linux filesystem, processes, or package installation? If so, use a container. Otherwise, prefer the smaller execution surface of an isolate when its runtime supports the task.
| Step | Typical fit |
|---|---|
| Generated tool function | Isolate |
| Constrained snippet interpreter | Isolate |
| Data transformation | Isolate |
| Application build or deployment | Container |
| Test suite needing a project environment | Container |
| Package installation or server execution | Container |
The requirement determines the environment; the label “AI agent” does not.
An agent can use both. Its tool loop generates a function, runs it in an isolate, returns the result to the model, and iterates. When the task becomes building an application, it switches to a container to clone the repository, install dependencies, and run the build. Isolates provide the fast iteration loop; containers provide the workbench. The choice applies to the current step rather than permanently to the entire agent.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Eight safeguards across both environments
The same operational checks apply whichever environment executes the code:
- Deny network access by default. Open only the routes the task requires, including any mediated routes that can transmit data.
- Grant explicit capabilities. Expose the minimum methods and data needed for the job.
- Isolate each user. Do not share execution environments across tenants.
- Limit resources. Set timeouts, memory caps, and CPU limits so broken code cannot consume the service.
- Keep secrets outside. Proxy sensitive operations through trusted application code.
- Clean up. Destroy finished environments, use
try/finallyfor bounded work, and set maximum lifetimes. - Record execution. Maintain an audit trail of what code ran, when it ran, who triggered it, and what it did.
- Validate before execution. Apply code-length limits, syntax checks, and checks for known dangerous patterns as defense in depth.
The final two checks make the system easier to investigate and reject some bad inputs early. They complement the execution boundary; they do not replace it.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Keep the trust boundary intact
The model that generates a working React component can also be induced to generate code that exfiltrates a database. Successful output does not establish permission to access the rest of the system. Treat generated code as you would code from an anonymous contributor: sandbox it, constrain it, and verify it each time.
The implementation paths are Dynamic Workers for constrained execution and the Sandbox SDK for full application environments. Agrawal also points to Code Mode as the AI-agent integration pattern used internally at Cloudflare. Whichever path supplies execution, the application remains responsible for defining the capabilities, tenant boundary, resource budget, and lifetime within which that code can act.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Resources
From the talk
Create Workers dynamically and control their code, bindings and network capabilities.
A TypeScript interface for running commands and managing files and processes in isolated environments.
Cloudflare's original explanation of generating code that calls APIs derived from MCP tools.
A prompt-based animation and video creation application with signup access.
Updates since the talk
Configure blocked outbound requests or route them through a gateway for filtering and credential injection.
Configure inactivity sleep, keep-alive behavior and explicit sandbox destruction.
Read the transcript and explore slides
Loading transcript and slides…