Skip to main content

vercel-sandbox

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.

Source facts

Repository
vercel/vercel-plugin
Last source activity
October 6, 2026 at 08:32
Detected SKILL.md language
English
Stars
301
Forks
64

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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 |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub