---
title: "Code sandboxes"
description: "Run untrusted or AI-written code in a sandbox with time, memory and cost limits, no secrets, and only the hosts you allow."
section: "interfaces"
status: "Building for 0.4 · ships as a beta API"
spec: "sandbox"
canonical: "https://mainsdk.dev/docs/interfaces/sandbox"
updated: "2026-10-28"
---

# Code sandboxes

> Run untrusted or AI-written code in a sandbox with time, memory and cost limits, no secrets, and only the hosts you allow.

Status: Building for 0.4 · ships as a beta API · Canonical: https://mainsdk.dev/docs/interfaces/sandbox · Updated: 2026-10-28

`sandbox` runs code you didn't write, from a user or a model, outside your app's process, with no secrets, files or network unless you allow them, and stops it at its limits.

Vercel Sandbox and E2B run it in microVMs in production; `local` runs a limited subprocess for development.

> **Note:** `sandbox` is in 0.4 (spec `sandbox`, ADR-0059). The `local`, `vercel-sandbox` and `e2b` drivers are in `mainsdk`; the provider drivers are tested against recorded sessions, not a live account. Browser automation and GPU workloads are planned and not part of this interface.

## At a glance

- Status: Building for 0.4 · ships as a beta API
- Phase: 1 (Core, public beta)
- Milestone: 0.4 Hardening
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/sandbox.md
- Category: Security and privacy

Calls:

- `sandbox.run({ code, language, files, network, feature })`
- `sandbox.create(opts)`
- `sandbox.get(id)`
- `sandbox.action({ name, intent, languages })`
- `new Sandbox(deps)`
- `createSandbox(deps)`
- `parseSandboxConfig(raw)`
- `secret(name)`
- `localSandboxDriver(opts)`
- `e2bNetworkPolicy(hosts)`
- `vercelNetworkPolicy(hosts)`

In main.yaml:

```yaml
sandbox:
  driver: vercel-sandbox
  limits: { timeout: 30s, memory: 512MB, output: 1MB, max-cost: $0.05 }
  network: [pypi.org]
```

Guarantees, whatever the driver:

- Code never runs in the app's process
- No network unless allowed, and never wider than network.allow
- No secrets or app values unless named
- Killed at its time, memory, output and cost limits
- Every run audited with the code's hash, never the code
- The local driver is refused in production


## Drivers

Pick one in main.yaml with `sandbox.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Vercel Sandbox | `vercel-sandbox` | yes |  |
| E2B | `e2b` |  |  |
| Local subprocess (development) | `local` |  |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-SANDBOX-0010` | check | `eval`, `new Function` or `node:vm` runs a value that isn't a string literal |
| `MAIN-SANDBOX-0011` | check | The `local` sandbox driver in production |
| `MAIN-SANDBOX-0012` | check | `sandbox.network` lists a host that `network.allow` doesn't |
| `MAIN-SANDBOX-0001` | runtime | A sandbox option is invalid (400); nothing ran |
| `MAIN-SANDBOX-0002` | runtime | The code reached its time limit and was killed |
| `MAIN-SANDBOX-0003` | runtime | The code passed its memory limit and was killed |
| `MAIN-SANDBOX-0004` | runtime | Output or files passed their limit; output is cut |
| `MAIN-SANDBOX-0005` | runtime | A host isn't in `sandbox.network` and `network.allow`, or the code tried a host it wasn't given (403) |
| `MAIN-SANDBOX-0006` | runtime | The sandbox driver is unavailable (503, retryable) |
| `MAIN-SANDBOX-0007` | runtime | The run would pass, or reached, its cost ceiling (402) |
| `MAIN-SANDBOX-0008` | runtime | No sandbox session with that id in the current tenant, or it was closed (404) |
| `MAIN-SANDBOX-0009` | runtime | A secret reference isn't under `sandbox.secrets` (403) |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| Local driver with time and memory limits, an empty environment and no network | 1 | 0.4 | `sandbox#local-driver` |
| Network allowlist parity with network.allow | 1 | 0.4 | `sandbox#network-parity` |
| No secrets unless named; files through files | 1 | 0.4 | `sandbox#secrets-files` |
| Sessions with run | 1 | 0.4 | `sandbox#sessions` |
| Vercel Sandbox and E2B drivers | 1 | 0.4 | `sandbox#provider-drivers` |
| A code-runner action for agents | 1 | 0.4 | `sandbox#action` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

```yaml title="main.yaml"
sandbox:
  driver: local                      # vercel-sandbox | e2b | local (development only)
  local: { python: python3 }
permissions:
  processes: [python3]               # the local driver starts code through the system interface
  disk: { temp: { max: 1GB } }       # and works in a temp folder it removes afterwards
```

```ts title="lib/run-snippet.ts"
import { sandbox } from "mainsdk";

export async function runSnippet(code: string) {
  const out = await sandbox.run({ code, language: "python", feature: "notebook" });
  return out.stop ? `stopped: ${out.stop}` : out.stdout;
}
```

`language` is `python`, `node` or `bash`. `feature` names the run in the audit event. A run that is stopped still returns a result with `stop` set; it does not throw.

## Configuration

```yaml title="main.yaml"
sandbox:
  driver: vercel-sandbox
  limits: { timeout: 30s, memory: 512MB, output: 1MB, files: 50MB, max-cost: $0.05 }
  network: [pypi.org]                # the most any run may get; must also be in network.allow
  secrets: [OPENWEATHER_KEY]         # the only secrets a run may be given
  vercel: { token: "secret:VERCEL_TOKEN", project: prj_123 }
network:
  allow: [pypi.org]
```

| Key | Meaning |
| --- | --- |
| `driver` | `vercel-sandbox`, `e2b` or `local`. Default `local`. |
| `limits` | `timeout` (30s), `memory` (512MB), `output` (1MB), `files` (50MB) and `max-cost` (none). A call can lower a limit, never raise it. |
| `network` | Hosts any run may be given. Each must also be in `network.allow`. A call's `network` defaults to none. |
| `secrets` | Names of secrets a run may receive through `secret(name)`. |
| `local` | Programs for the `local` driver: `{ python: python3, node: node }`. List them under `permissions.processes` too. |
| `vercel` | `token` (default `secret:VERCEL_TOKEN`), `project`, optional `team` and `runtime`. |
| `e2b` | `key` (default `secret:E2B_API_KEY`) and optional `template`. |
| `price` | Price per second of a box, such as `$0.0001`, to override a driver's estimate. |

An unknown key under `sandbox` is refused at load with the key named. The provider's API host is added to the network list for you.

## What you get

- Code never runs in your app's process, and starts with an empty environment, an empty `/work` holding only the files you pass, and an empty `/out`.
- No network by default. Each host you give must be in both `sandbox.network` and `network.allow`, or the call fails before anything runs.
- Output is cut at `limits.output` and redacted for secret values and known key formats (it comes back as `[redacted:secret]`).
- The worst-case cost (price per second times the timeout) is checked against `maxCost` and `limits.max-cost` before the run starts.
- One audit event per run with the SHA-256 of the code, driver, isolation, duration, exit code, stop reason, hosts and env names. Never the code, output or secret values.
- A provider outage fails only sandbox calls (`MAIN-SANDBOX-0006`), not the rest of your app.

## Examples

### Pass files in and read results out

Files you pass appear in the code's working folder (`/work`); files it writes to the `out` folder next to it (`../out/` on the `local` driver) come back as `FileRef`s owned by the caller.

```ts title="lib/chart.ts"
import { sandbox } from "mainsdk";

export async function chart(csv: string) {
  const out = await sandbox.run({
    language: "python",
    feature: "chart",
    files: { "data.csv": csv },
    code: "rows = open('data.csv').read().splitlines()\nopen('../out/count.txt', 'w').write(str(len(rows)))",
  });
  return out.files;   // [{ id, ... }] for count.txt
}
```

### Give a run one secret and one host

```ts title="lib/weather.ts"
import { sandbox, secret } from "mainsdk";

export function weather(code: string) {
  return sandbox.run({
    code,
    language: "python",
    feature: "weather",
    network: ["pypi.org"],
    env: { WEATHER_KEY: secret("OPENWEATHER_KEY") },
  });
}
```

A name that isn't under `sandbox.secrets` fails with `MAIN-SANDBOX-0009` and nothing runs.

### Keep a session open across commands

A session is scoped to the tenant that created it, closes after `idle` (5 minutes by default), and runs commands as a program and an argument array, never through a shell.

```ts title="lib/session.ts"
import { sandbox } from "mainsdk";

export async function twoSteps() {
  const session = await sandbox.create({ feature: "notebook", idle: "1m" });
  try {
    await session.write("a.txt", "hello");
    const out = await session.run("python3", ["-c", "print(open('a.txt').read())"]);
    return out.stdout;
  } finally {
    await session.close();
  }
}
```

`sandbox.get(id)` returns an open session of the current tenant.

### Let an agent run code

`sandbox.action` returns a code-runner action. Its input is `{ code, language, files? }`, its risk is `write` unless you set it, and an agent calling it passes the agent's budget and approval rules. Its cost counts against the agent's budget.

```ts title="lib/agent.ts"
import { ai, sandbox } from "mainsdk";

const runPython = sandbox.action({
  name: "code.run",
  intent: "Run Python to analyse the user's data",
  languages: ["python"],
});

export function analyse(user: { id: string; roles: string[] }, goal: string) {
  return ai.run({ model: "anthropic/claude-sonnet-4-5", feature: "analysis", agent: "analyst", user, goal, tools: [ai.tool(runPython)] });
}
```

## Reference

Most apps use the `sandbox` export, which `main.yaml` builds for you. These are the pieces underneath, for tests and custom wiring.

| Call | What it does |
| --- | --- |
| `new Sandbox(deps)` | The interface class. `deps` takes the `driver`, the parsed `config`, the `network` matcher and a `secret` resolver, plus optional `files`, `log`, `tenancy`, `agents` and `access`. |
| `createSandbox(deps)` | Builds a `Sandbox` from the same `deps`. |
| `parseSandboxConfig(raw)` | Reads the `sandbox` section of `main.yaml`, fills the defaults and refuses an unknown key (`MAIN-CONFIG-0003`) or an invalid option (`MAIN-SANDBOX-0001`). |
| `localSandboxDriver({ permissions, programs? })` | The `local` driver: a limited subprocess for development and tests. `programs` sets the `python` and `node` paths. |
| `e2bNetworkPolicy(hosts)` | The E2B egress policy: `{ allowOut: hosts, denyOut: ["0.0.0.0/0"] }`, so only the given hosts are reachable. |
| `vercelNetworkPolicy(hosts)` | The Vercel Sandbox egress policy: `{ mode: "deny-all" }` for no hosts, otherwise `{ mode: "custom", allowedDomains: hosts }`. |

## Isolation you can read

Every result has `isolation`: `microvm` for the providers, `namespace` for the `local` driver on Linux with an empty network, and `weak` for the `local` driver otherwise (a proxy refuses other hosts; the code is not in a separate network). Treat `weak` as a development convenience.

## Errors and checks

| Code | When | Fix |
| --- | --- | --- |
| `MAIN-SANDBOX-0001` | An option is invalid: unknown language, empty code, a limit above `sandbox.limits`, a path outside `/work` or `/out`. | Fix the option; nothing ran. |
| `MAIN-SANDBOX-0002` to `0004`, `0007` | The run hit its time, memory, output or cost limit. Returned as `stop` (`timeout`, `memory`, `output`, `cost`), not thrown; `0007` throws before the run when the worst case is over `maxCost`. | Raise the limit in `main.yaml` or lower the work. |
| `MAIN-SANDBOX-0005` | A host is not in `sandbox.network` and `network.allow`, or the code tried a host it wasn't given (logged). | Add it to both lists, or drop it from the call. |
| `MAIN-SANDBOX-0006` | The driver is unavailable (503, retryable). | Retry; check the provider and token. |
| `MAIN-SANDBOX-0008` | No open session with that id in the current tenant. | Create a new session. |
| `MAIN-SANDBOX-0009` | A `secret()` names a secret that isn't under `sandbox.secrets`. | List it there. |

`mainsdk check` reports `MAIN-SANDBOX-0010` for `eval`, `new Function` or `vm.*` on anything but a string literal, `MAIN-SANDBOX-0011` for the `local` driver in production, and `MAIN-SANDBOX-0012` for a `sandbox.network` host missing from `network.allow`. See [check codes](https://mainsdk.dev/docs/reference/check-codes).

## Swapping drivers

Change `sandbox.driver` and nothing in your code changes: `vercel-sandbox` and `e2b` run code in microVMs through the provider's API (token or key as a secret reference), and `local` runs a subprocess for development. Every driver gets the same limits, the same network allowlist (enforced through the provider's egress policy) and the same audit event. `local` in production fails `mainsdk check --env prod` with `MAIN-SANDBOX-0011`.

## Testing

The `local` driver needs no account; run the real code in a test and assert on `stdout`, `stop` and `files`. Put the interpreters under `permissions.processes` in the test's `main.yaml`. The provider drivers are covered by the conformance suite on recorded sessions.

## Security and privacy

- Code never runs in your app's process.
- Secrets reach a run only when listed under `sandbox.secrets` and passed by name; the audit event records names, never values.
- Every run is audited with the SHA-256 of the code, never the code or its output.
- Sessions of one tenant can't be reached from another.

## Next

- [Network permissions](https://mainsdk.dev/docs/interfaces/network)
- [Actions and agent tools](https://mainsdk.dev/docs/interfaces/action)
- [Server facts and programs](https://mainsdk.dev/docs/interfaces/system)
