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.

Slide titled “We’re All Doing This” with five bullets covering autonomous agents, tool-calling loops, coding assistants, delegated workflows, and their power.Open full source frame
AI agents and coding assistants increasingly execute code on our behalf.

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.

0:280:47
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

0:28 · section reference included

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.

Slide titled “Three Threat Scenarios” with three cards listing broken-code behaviors, overly helpful access to environment variables and internal URLs, and prompt-injection routes. A presenter inset appears at bottom right.Open full source frame
Three threat scenarios: the hallucinating LLM, the “helpful” LLM, and the compromised prompt.

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.

3:073:18
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

3:07 · section reference included

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.

ApproachWhat you must specifyFailure mode
BlocklistEvery forbidden operationAn omitted operation remains available
Explicit capabilitiesOnly permitted operationsUngranted 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.

6:557:05
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

6:55 · section reference included

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.

10:0010:11
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

10:00 · section reference included

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.

“Two Approaches” slide comparing isolates and containers by startup speed, supported execution, filesystem access, and suitable workloads.Open full source frame
Two approaches: lightweight isolates and full Linux 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.

13:0813:20
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

13:08 · section reference included

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.

15:2215:38
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

15:22 · section reference included

Make networking an explicit decision

Network access has three useful policy levels:

PolicyBehaviorAppropriate use
BlockedglobalOutbound: nullCode needs no direct network access
MediatedRequests pass through an owned serviceApproved APIs or webhooks
UnrestrictedCode chooses its destinationsAvoid 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.

18:4318:58
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

18:43 · section reference included

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.

19:5220:09
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

19:52 · section reference included

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.

Dark application interface with a white video preview reading “Generated with images-ai.app” and a yellow highlight beneath part of the text.Open full source frame
The demo displays a generated video preview.

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.

RequirementNeeded capability
Clone the starter repositoryGit and writable files
Install dependenciesPackage manager and processes
Run the development serverLong-lived process and listening port
Return a preview URLReachable 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.

21:1221:25
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

21:12 · section reference included

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:

  1. Select the user’s sandbox. The user identifier determines which isolated environment receives the work.
  2. Clone the starter repository inside it. Git writes into the container’s filesystem.
  3. Install dependencies inside it. The parent Worker does not install or execute the generated application’s packages in its own environment.
  4. Start the development server in the background. The process remains alive to serve the preview.
  5. 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.

23:2423:38
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

23:24 · section reference included

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.

26:1626:22
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

26:16 · section reference included

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.

“Cleanup” slide with a try/finally code example and a “Why this matters” box covering running costs, data persisting on disk, and guaranteed cleanup.Open full source frame
Container cleanup addresses running costs and residual data exposure.

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.

28:3828:59
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

28:38 · section reference included

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.

StepTypical fit
Generated tool functionIsolate
Constrained snippet interpreterIsolate
Data transformationIsolate
Application build or deploymentContainer
Test suite needing a project environmentContainer
Package installation or server executionContainer

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.

31:0331:20
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

31:03 · section reference included

Eight safeguards across both environments

The same operational checks apply whichever environment executes the code:

  1. Deny network access by default. Open only the routes the task requires, including any mediated routes that can transmit data.
  2. Grant explicit capabilities. Expose the minimum methods and data needed for the job.
  3. Isolate each user. Do not share execution environments across tenants.
  4. Limit resources. Set timeouts, memory caps, and CPU limits so broken code cannot consume the service.
  5. Keep secrets outside. Proxy sensitive operations through trusted application code.
  6. Clean up. Destroy finished environments, use try/finally for bounded work, and set maximum lifetimes.
  7. Record execution. Maintain an audit trail of what code ran, when it ran, who triggered it, and what it did.
  8. 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.

33:1633:38
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

33:16 · section reference included

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.

Slide stating “AI-generated code is untrusted code,” warning that an LLM writing React components can be tricked into exfiltrating data, and concluding “Treat it accordingly.”Open full source frame
AI-generated code is untrusted code.

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.

35:3435:56
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

35:34 · section reference included

Resources

From the talk

Updates since the talk

Transcript download
Read the transcript and explore slides

Loading transcript and slides…