- name
- vercel-sandbox
- description
- Vercel Sandbox guidance — ephemeral Firecracker microVMs for running untrusted code safely. Supports AI agents, code generation, and experimentation. Use when executing user-generated or AI-generated code in isolation.
- summary
- Run untrusted/AI-generated code in ephemeral Firecracker microVMs via @vercel/sandbox. Core loop: `const s = await Sandbox.create(); try { const r = await s.runCommand('python3', ['-c', code]); } finally { await s.stop(); }`. runCommand has no shell (wrap pipes/redirects in `bash -c`) and does not throw on non-zero exit (check r.exitCode). Default image is Ubuntu (`apt-get update` before install). Persistence is on by default (auto-snapshot on stop, resume by name; only the filesystem survives). For untrusted code use `networkPolicy: 'deny-all'`, a short `timeout`, and `persistent: false`. Credential brokering: a firewall `transform` injects a secret header on egress so the VM never holds it. AI agents reach models with no API key via the AI Gateway (`https://ai-gateway.vercel.sh`, `Authorization: Bearer $VERCEL_OIDC_TOKEN`) — the token is not auto-injected, so pass it via `env` or broker it. Full docs: https://vercel.com/docs/sandbox
- metadata
- {"priority":4,"docs":["https://vercel.com/docs/sandbox"],"sitemap":"https://vercel.com/sitemap.xml","pathPatterns":[],"importPatterns":["@vercel/sandbox"],"bashPatterns":["\\bnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b","\\bpnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b","\\bbun\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b","\\byarn\\s+add\\s+[^\\n]*@vercel/sandbox\\b"],"promptSignals":{"phrases":["@vercel/sandbox","sandbox","code sandbox","vercel sandbox","isolated environment","sandboxed execution"],"allOf":["[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]"],"anyOf":["sandbox","isolated","isolation","untrusted","safely","microvm","ffmpeg","playground"],"noneOf":["iframe sandbox","sandbox attribute","codesandbox.io","stackblitz"],"minScore":4}}
- retrieval
- {"aliases":["code sandbox","microvm","isolated execution","safe code runner"],"intents":["run untrusted code","execute code safely","create sandbox","isolate code execution"],"entities":["Vercel Sandbox","Firecracker","microVM","isolated execution"]}
- chainTo
- [{"pattern":"from\\s+['\"\"]vm2['\"\"]|require\\s*\\(\\s*['\"\"]vm2['\"\"\\)]|new\\s+VM\\(","targetSkill":"vercel-sandbox","message":"vm2 detected — it has known security vulnerabilities. Reloading Vercel Sandbox guidance for Firecracker microVM-based safe execution."},{"pattern":"child_process.*exec\\(|execSync\\(|spawn\\(.*\\{.*shell:\\s*true","targetSkill":"ai-sdk","message":"Shell exec for code execution detected — loading AI SDK guidance for tool-calling patterns that pair with Vercel Sandbox for safe agent execution."}]
# Vercel Sandbox
Vercel Sandbox runs untrusted or AI-generated code inside an ephemeral Firecracker microVM. You get a real Linux VM with a filesystem and network — created on demand over an API, and stopped (or snapshotted) when you're done. Reach for it when code you don't fully trust needs to run: AI agent tool calls, code generation, user submissions, builds, or experiments.
Do **not** use in-process sandboxes like `vm2` (known escapes) or `child_process`/`eval` for untrusted code. Those share your process; a Sandbox is a separate VM.
## Install
```bash
pnpm add @vercel/sandbox # or npm i / yarn add / bun add
```
There is also a Python SDK (`vercel` package, `vercel.sandbox`) and a `sandbox` CLI. This skill shows the JS SDK unless noted.
## Minimal example
The core loop is create → run → stop. For one-off work, stop in a `finally` so a thrown error can't leak a running VM (you're billed while it runs). `stop()` is safe to call more than once.
```ts
import { Sandbox } from "@vercel/sandbox";
const sandbox = await Sandbox.create();
try {
const result = await sandbox.runCommand("python3", ["-c", "print(2 + 2)"]);
console.log(await result.stdout()); // "4\n"
console.log(result.exitCode); // 0
} finally {
await sandbox.stop();
}
```
`Sandbox.create()` with no arguments boots the default image (`vercel/sandbox/universal`, Ubuntu with Node.js 24, Python 3.14 as `python3`, and common tools), 2 vCPUs, and a 5-minute timeout.
## Authentication
- **On Vercel** (Functions, Cron, builds): the SDK authenticates automatically via the deployment's OIDC token. No config.
- **Local dev**: run `vercel link` then `vercel env pull` to get a `VERCEL_OIDC_TOKEN` in `.env.local` (valid ~12h; re-pull when it expires).
- **External / CI** (no OIDC available): set `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID` and pass them as `token`, `teamId`, `projectId` to `Sandbox.create()` (the SDK does not read them from the environment).
This is auth for the process **calling** the SDK. It is separate from any credential you want available **inside** the VM — the sandbox does not automatically carry your `VERCEL_OIDC_TOKEN` (see [Running AI agents](#running-ai-agents-in-a-sandbox)).
## Creating a sandbox
Common `Sandbox.create()` options (all optional):
| Option | Default | Notes |
|---|---|---|
| `image` | `vercel/sandbox/universal` | Managed image, or a custom/public VCR image. See [Images](#images). |
| `resources` | `{ vcpus: 2 }` | `vcpus` can be `1` or an even number up to the plan max (Hobby 4, Pro 8, Enterprise 32). Each vCPU includes 2 GB RAM. Use `1` for cheap, low-intensity untrusted runs. |
| `timeout` | `300_000` (5 min) | Session timeout in ms. When it elapses the session is stopped and any in-flight `runCommand` **rejects**. Extend with `sandbox.extendTimeout(ms)` up to the plan max session (Hobby 45 min, Pro/Ent 24h). |
| `ports` | `[]` | Ports to expose, up to 15. Reach them with `sandbox.domain(port)`. Your server must listen on `0.0.0.0` (not `127.0.0.1`) to be reachable. |
| `region` | project default or `iad1` | One of 19 regions. |
| `persistent` | `true` | Auto-snapshots on stop and resumes on next call. Pass `false` for one-off work to avoid snapshot storage cost. |
| `networkPolicy` | `"allow-all"` | Use `"deny-all"` or an allow-list for untrusted code. See [Network policy](#network-policy-and-credential-brokering). |
| `env` | – | Environment variables for every command. Per-command `env` overrides these. Use this to inject a credential into the VM. |
| `name` | auto-generated | Unique per project, immutable. Used to retrieve/resume a persistent sandbox. |
| `tags` | – | Up to 5 key-value pairs for filtering in `Sandbox.list()`. |
## Running commands
`runCommand` runs a binary directly — **there is no shell**, so pipes, redirects, `&&`, and globs do not work unless you invoke a shell yourself. It **resolves with the finished command regardless of exit code** (it does not throw on a non-zero exit); check `result.exitCode`. It only rejects on an actual failure to run — e.g. the session timing out mid-command.
Commands run as a **non-root** user (`ubuntu`, in the sudo group) by default; pass `sudo: true` for root.
```ts
const r = await sandbox.runCommand("npm", ["install"]);
if (r.exitCode !== 0) throw new Error(await r.stderr()); // non-zero does NOT throw
// Needs a shell for the redirect / pipe. Use absolute paths (see below).
await sandbox.runCommand("bash", ["-c", "echo hi > /vercel/sandbox/out.txt && cat /vercel/sandbox/out.txt"]);
// Root for one command (sudo is object-form only)
await sandbox.runCommand({ cmd: "apt-get", args: ["update"], sudo: true });
// Long-running process: detached (object form only) returns immediately
const server = await sandbox.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true });
```
`runCommand` returns a finished command with `await result.stdout()`, `await result.stderr()`, and `result.exitCode` (or a live `Command` when `detached: true`). The object form also takes `cwd`, `env`, and `stdout`/`stderr` (a `Writable` to stream into). `sudo` and `detached` are object-form only.
**Working directory**: the file methods below are rooted at `/vercel/sandbox`, but do not assume a command's default working directory is the same. Whenever a command reads or writes files you created with `writeFiles`/`readFileToBuffer`, use **absolute paths under `/vercel/sandbox`** (or pass an explicit `cwd`) so both sides point at the same place.
## Files
File-method paths are relative to `/vercel/sandbox` unless absolute. `content` must be a `Buffer`.
```ts
await sandbox.writeFiles([
{ path: "input.txt", content: Buffer.from("line one\nline two\n") },
{ path: "run.sh", content: Buffer.from("#!/bin/bash\necho hi"), mode: 0o755 },
]);
// readFileToBuffer returns a Buffer, or null if the file is missing — guard it.
// (readFile returns a ReadableStream; neither returns a string, so convert yourself.)
const buf = await sandbox.readFileToBuffer({ path: "input.txt" });
const text = buf?.toString("utf8") ?? "";
await sandbox.mkDir("src/generated");
```
To pull source in at create time, use `source`: a git repo (`{ type: "git", url, username, password, depth?, revision? }` — `username`/`password` authenticate a private repo), a `tarball` (`{ type: "tarball", url }`), or a `snapshot` (`{ type: "snapshot", snapshotId }`).
## Installing system packages
The default image is **Ubuntu** — use `apt-get`, and run `apt-get update` first (package lists ship empty, so install fails without it). This needs `sudo`, and there is no shell, so run it through `bash -c` and check the exit code:
```ts
const install = await sandbox.runCommand({
cmd: "bash",
args: ["-c", "apt-get update && apt-get install -y ffmpeg"],
sudo: true,
});
if (install.exitCode !== 0) throw new Error(await install.stderr());
```
For a different base, use a managed image (`vercel/sandbox/arch` uses `pacman`/`yay`) or build a [custom image](#images) so packages are baked in and there's nothing to install at runtime.
## Ports and preview URLs
Expose ports at create time (up to 15), start a server **listening on `0.0.0.0`** (not `127.0.0.1`, or it's unreachable — the host flag is framework-specific), then read its public URL. `detached` returns when the process spawns, not when it's listening, so poll for readiness before using the URL:
```ts
const sandbox = await Sandbox.create({ ports: [3000] });
// Bind 0.0.0.0 — e.g. Next/Vite: `run dev -- --host 0.0.0.0`; node http: listen("0.0.0.0")
await sandbox.runCommand({ cmd: "npm", args: ["run", "dev", "--", "--host", "0.0.0.0"], detached: true });
// Wait until the port actually answers inside the VM
for (let i = 0; i < 30; i++) {
const ping = await sandbox.runCommand("bash", ["-c", "curl -sf http://localhost:3000 >/dev/null && echo up || true"]);
if ((await ping.stdout()).includes("up")) break;
await new Promise((r) => setTimeout(r, 1000));
}
const url = sandbox.domain(3000); // public HTTPS URL for port 3000
```
The URL is served by the running session. If the sandbox is stopped, nothing is listening until you resume it and restart the server — so for a durable preview keep the session alive (`extendTimeout`) rather than relying on the URL between sessions. Traffic to and from exposed ports is billable (requests and responses both count).
## Lifecycle and persistence
**Persistence is the default.** When a persistent sandbox stops, its **filesystem** is snapshotted automatically; a later call resumes it into a fresh session. Only the filesystem is saved — **running processes do not survive a stop/resume**, so restart long-running servers on resume (see below).
- **Sandbox vs session**: a *sandbox* is a long-lived entity identified by `name`; a *session* is one VM boot. The max session duration caps each session, not the sandbox — resuming starts a new session with a fresh timeout, so a persistent sandbox's total lifetime is effectively unbounded.
- **Retrieve / resume**: `Sandbox.get({ name })` returns the handle immediately and auto-resumes on the next call that needs a running VM (`resume: false` only skips resuming inside `get`; it doesn't disable this). `getOrCreate` does not resume before returning by default; pass `resume: true` to resume and await `onResume` immediately. `stop()` and `update()` never auto-resume. Use `getOrCreate` when the sandbox may not exist yet, `get` when you know it does.
- **`getOrCreate` accepts the same create options** as `create` (`ports`, `persistent`, `resources`, `networkPolicy`, `env`, …). They apply **only when it creates** the sandbox; if the named sandbox already exists it's returned with its existing config (use `sandbox.update({ … })` to change it).
- **Hooks are per call**, and fire on mutually exclusive events: `onCreate` runs once, the first time `getOrCreate` creates the sandbox; `onResume` runs on a resume. So to start a service **exactly once per session**, start it in **both** `onCreate` (first boot) and `onResume` (later boots). Hooks are arguments to *this* `getOrCreate` call, not stored on the sandbox — a *different process* resuming via `Sandbox.get` won't run them, so restart what it needs itself.
- A detached server returns as soon as the process spawns, **not** when it's listening — so after starting it (in `onCreate` for the first boot and `onResume` for later ones) poll until the port answers before treating `domain(port)` as live (see [Ports](#ports-and-preview-urls)).
```ts
const startDev = (s) =>
s.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true, cwd: "/vercel/sandbox" });
const sandbox = await Sandbox.getOrCreate({
name: "agent-ws",
ports: [3000],
onCreate: async (s) => { // once, on first creation
await s.runCommand({ cmd: "git", args: ["clone", repoUrl, "."], cwd: "/vercel/sandbox" });
await s.runCommand({ cmd: "npm", args: ["install"], cwd: "/vercel/sandbox" });
await startDev(s); // up on first boot, before domain() is read
},
onResume: async (s) => startDev(s), // every later resume
});
// Poll for the server to be listening (see Ports) before using the URL.
const url = sandbox.domain(3000);
// Don't stop this sandbox in a finally — stopping kills the dev server and the URL.
// If this process might find the sandbox already existing (not freshly created), pass
// `resume: true` above and start the server yourself — the hooks only fire on create/this call.
```
A separate later process reconnects by name and resumes on the first command. It won't run the hooks above, so restart anything it needs:
```ts
const sandbox = await Sandbox.get({ name: "agent-ws" });
const test = await sandbox.runCommand({ cmd: "npm", args: ["test"], cwd: "/vercel/sandbox" }); // resumes, then runs
```
Opt out for one-off work: `Sandbox.create({ persistent: false })` — the filesystem is discarded on stop and you accrue no snapshot-storage cost. Recommended for scratch/CI tasks.
## Snapshots
A snapshot is a saved full-filesystem image you can boot new sandboxes from — the way to skip repeated dependency installs (create-from-snapshot is much faster than installing from scratch).
```ts
const running = await Sandbox.create({ image: "vercel/sandbox/node:24" });
await running.runCommand({ cmd: "bash", args: ["-c", "apt-get update && apt-get install -y ffmpeg"], sudo: true });
const snap = await running.snapshot(); // sandbox stops automatically after; do NOT call stop()
const fast = await Sandbox.create({ source: { type: "snapshot", snapshotId: snap.snapshotId } });
```
Snapshots expire 30 days after last use by default. Control retention with `snapshotExpiration` (ms; `0` = never) and `keepLastSnapshots: { count: 1 }` (keep only the latest — keeps storage flat). Persistent sandboxes create these automatically on stop.
## Images
Pass `image` to control the environment. Managed images live under `vercel/sandbox`:
| Image | Contents |
|---|---|
| `vercel/sandbox/universal` (default) | Ubuntu + Node.js 24, Python 3.14, coding agents, utilities |
| `vercel/sandbox/node:22\|24\|26` | Ubuntu + pinned Node.js, pnpm |
| `vercel/sandbox/python:3.14` | Ubuntu + Python 3.14, pip, venv, uv |
| `vercel/sandbox/ubuntu` | Minimal Ubuntu 26.04 + sudo |
| `vercel/sandbox/arch` | Arch Linux, yay, base-devel |
عرض على GitHub