{"id":27069,"plugin_id":"plugin_connector_690a90ec05c881918afb6a55dc9bbaa1","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-06T18:03:11.742Z","digest":"b6169be30d084bb784f0cb526a5e83bedc42eac31d1bf7e9f6ac77c16790c390","against":24896,"payload":{"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.","included_files":[{"relative_path":"agents/openai.yaml","size_in_bytes":160}],"name":"vercel-sandbox","skill_md_contents":"---\nname: vercel-sandbox\ndescription: 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.\nsummary: \"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\"\nmetadata:\n  priority: 4\n  docs:\n    - \"https://vercel.com/docs/sandbox\"\n  sitemap: \"https://vercel.com/sitemap.xml\"\n  pathPatterns: []\n  importPatterns:\n    - '@vercel/sandbox'\n  bashPatterns:\n    - '\\bnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\bpnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\bbun\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\byarn\\s+add\\s+[^\\n]*@vercel/sandbox\\b'\n  promptSignals:\n    phrases:\n      - \"@vercel/sandbox\"\n      - \"sandbox\"\n      - \"code sandbox\"\n      - \"vercel sandbox\"\n      - \"isolated environment\"\n      - \"sandboxed execution\"\n    allOf:\n      - [sandbox, code]\n      - [sandbox, execute]\n      - [sandbox, run]\n      - [sandbox, isolated]\n      - [sandbox, safe]\n      - [sandbox, environment]\n      - [isolated, execute]\n      - [isolated, code]\n      - [isolated, environment]\n      - [isolated, run]\n      - [safe, execute]\n      - [safe, code]\n      - [untrusted, code]\n      - [untrusted, execute]\n      - [code, runner]\n      - [code, playground]\n      - [execute, safely]\n      - [run, safely]\n      - [run, isolation]\n      - [execute, isolation]\n      - [ffmpeg, process]\n      - [ffmpeg, convert]\n      - [ffmpeg, compress]\n      - [student, code]\n      - [student, execute]\n      - [student, run]\n    anyOf:\n      - \"sandbox\"\n      - \"isolated\"\n      - \"isolation\"\n      - \"untrusted\"\n      - \"safely\"\n      - \"microvm\"\n      - \"ffmpeg\"\n      - \"playground\"\n    noneOf:\n      - \"iframe sandbox\"\n      - \"sandbox attribute\"\n      - \"codesandbox.io\"\n      - \"stackblitz\"\n    minScore: 4\nretrieval:\n  aliases:\n    - code sandbox\n    - microvm\n    - isolated execution\n    - safe code runner\n  intents:\n    - run untrusted code\n    - execute code safely\n    - create sandbox\n    - isolate code execution\n  entities:\n    - Vercel Sandbox\n    - Firecracker\n    - microVM\n    - isolated execution\nchainTo:\n  -\n    pattern: 'from\\s+[''\"\"]vm2[''\"\"]|require\\s*\\(\\s*[''\"\"]vm2[''\"\"\\)]|new\\s+VM\\('\n    targetSkill: vercel-sandbox\n    message: 'vm2 detected — it has known security vulnerabilities. Reloading Vercel Sandbox guidance for Firecracker microVM-based safe execution.'\n  -\n    pattern: 'child_process.*exec\\(|execSync\\(|spawn\\(.*\\{.*shell:\\s*true'\n    targetSkill: ai-sdk\n    message: 'Shell exec for code execution detected — loading AI SDK guidance for tool-calling patterns that pair with Vercel Sandbox for safe agent execution.'\n\n---\n\n# Vercel Sandbox\n\nVercel 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.\n\nDo **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.\n\n## Install\n\n```bash\npnpm add @vercel/sandbox   # or npm i / yarn add / bun add\n```\n\nThere is also a Python SDK (`vercel` package, `vercel.sandbox`) and a `sandbox` CLI. This skill shows the JS SDK unless noted.\n\n## Minimal example\n\nThe 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.\n\n```ts\nimport { Sandbox } from \"@vercel/sandbox\";\n\nconst sandbox = await Sandbox.create();\ntry {\n  const result = await sandbox.runCommand(\"python3\", [\"-c\", \"print(2 + 2)\"]);\n  console.log(await result.stdout()); // \"4\\n\"\n  console.log(result.exitCode);       // 0\n} finally {\n  await sandbox.stop();\n}\n```\n\n`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.\n\n## Authentication\n\n- **On Vercel** (Functions, Cron, builds): the SDK authenticates automatically via the deployment's OIDC token. No config.\n- **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).\n- **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).\n\nThis 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)).\n\n## Creating a sandbox\n\nCommon `Sandbox.create()` options (all optional):\n\n| Option | Default | Notes |\n|---|---|---|\n| `image` | `vercel/sandbox/universal` | Managed image, or a custom/public VCR image. See [Images](#images). |\n| `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. |\n| `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). |\n| `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. |\n| `region` | project default or `iad1` | One of 19 regions. |\n| `persistent` | `true` | Auto-snapshots on stop and resumes on next call. Pass `false` for one-off work to avoid snapshot storage cost. |\n| `networkPolicy` | `\"allow-all\"` | Use `\"deny-all\"` or an allow-list for untrusted code. See [Network policy](#network-policy-and-credential-brokering). |\n| `env` | – | Environment variables for every command. Per-command `env` overrides these. Use this to inject a credential into the VM. |\n| `name` | auto-generated | Unique per project, immutable. Used to retrieve/resume a persistent sandbox. |\n| `tags` | – | Up to 5 key-value pairs for filtering in `Sandbox.list()`. |\n\n## Running commands\n\n`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.\n\nCommands run as a **non-root** user (`ubuntu`, in the sudo group) by default; pass `sudo: true` for root.\n\n```ts\nconst r = await sandbox.runCommand(\"npm\", [\"install\"]);\nif (r.exitCode !== 0) throw new Error(await r.stderr()); // non-zero does NOT throw\n\n// Needs a shell for the redirect / pipe. Use absolute paths (see below).\nawait sandbox.runCommand(\"bash\", [\"-c\", \"echo hi > /vercel/sandbox/out.txt && cat /vercel/sandbox/out.txt\"]);\n\n// Root for one command (sudo is object-form only)\nawait sandbox.runCommand({ cmd: \"apt-get\", args: [\"update\"], sudo: true });\n\n// Long-running process: detached (object form only) returns immediately\nconst server = await sandbox.runCommand({ cmd: \"npm\", args: [\"run\", \"dev\"], detached: true });\n```\n\n`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.\n\n**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.\n\n## Files\n\nFile-method paths are relative to `/vercel/sandbox` unless absolute. `content` must be a `Buffer`.\n\n```ts\nawait sandbox.writeFiles([\n  { path: \"input.txt\", content: Buffer.from(\"line one\\nline two\\n\") },\n  { path: \"run.sh\", content: Buffer.from(\"#!/bin/bash\\necho hi\"), mode: 0o755 },\n]);\n\n// readFileToBuffer returns a Buffer, or null if the file is missing — guard it.\n// (readFile returns a ReadableStream; neither returns a string, so convert yourself.)\nconst buf = await sandbox.readFileToBuffer({ path: \"input.txt\" });\nconst text = buf?.toString(\"utf8\") ?? \"\";\n\nawait sandbox.mkDir(\"src/generated\");\n```\n\nTo 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 }`).\n\n## Installing system packages\n\nThe 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:\n\n```ts\nconst install = await sandbox.runCommand({\n  cmd: \"bash\",\n  args: [\"-c\", \"apt-get update && apt-get install -y ffmpeg\"],\n  sudo: true,\n});\nif (install.exitCode !== 0) throw new Error(await install.stderr());\n```\n\nFor 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.\n\n## Ports and preview URLs\n\nExpose 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:\n\n```ts\nconst sandbox = await Sandbox.create({ ports: [3000] });\n// Bind 0.0.0.0 — e.g. Next/Vite: `run dev -- --host 0.0.0.0`; node http: listen(\"0.0.0.0\")\nawait sandbox.runCommand({ cmd: \"npm\", args: [\"run\", \"dev\", \"--\", \"--host\", \"0.0.0.0\"], detached: true });\n\n// Wait until the port actually answers inside the VM\nfor (let i = 0; i < 30; i++) {\n  const ping = await sandbox.runCommand(\"bash\", [\"-c\", \"curl -sf http://localhost:3000 >/dev/null && echo up || true\"]);\n  if ((await ping.stdout()).includes(\"up\")) break;\n  await new Promise((r) => setTimeout(r, 1000));\n}\nconst url = sandbox.domain(3000); // public HTTPS URL for port 3000\n```\n\nThe 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).\n\n## Lifecycle and persistence\n\n**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).\n\n- **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.\n- **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.\n- **`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).\n- **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.\n- 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)).\n\n```ts\nconst startDev = (s) =>\n  s.runCommand({ cmd: \"npm\", args: [\"run\", \"dev\"], detached: true, cwd: \"/vercel/sandbox\" });\n\nconst sandbox = await Sandbox.getOrCreate({\n  name: \"agent-ws\",\n  ports: [3000],\n  onCreate: async (s) => {          // once, on first creation\n    await s.runCommand({ cmd: \"git\", args: [\"clone\", repoUrl, \".\"], cwd: \"/vercel/sandbox\" });\n    await s.runCommand({ cmd: \"npm\", args: [\"install\"], cwd: \"/vercel/sandbox\" });\n    await startDev(s);              // up on first boot, before domain() is read\n  },\n  onResume: async (s) => startDev(s), // every later resume\n});\n// Poll for the server to be listening (see Ports) before using the URL.\nconst url = sandbox.domain(3000);\n// Don't stop this sandbox in a finally — stopping kills the dev server and the URL.\n// If this process might find the sandbox already existing (not freshly created), pass\n// `resume: true` above and start the server yourself — the hooks only fire on create/this call.\n```\n\nA separate later process reconnects by name and resumes on the first command. It won't run the hooks above, so restart anything it needs:\n\n```ts\nconst sandbox = await Sandbox.get({ name: \"agent-ws\" });\nconst test = await sandbox.runCommand({ cmd: \"npm\", args: [\"test\"], cwd: \"/vercel/sandbox\" }); // resumes, then runs\n```\n\nOpt 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.\n\n## Snapshots\n\nA 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).\n\n```ts\nconst running = await Sandbox.create({ image: \"vercel/sandbox/node:24\" });\nawait running.runCommand({ cmd: \"bash\", args: [\"-c\", \"apt-get update && apt-get install -y ffmpeg\"], sudo: true });\nconst snap = await running.snapshot(); // sandbox stops automatically after; do NOT call stop()\n\nconst fast = await Sandbox.create({ source: { type: \"snapshot\", snapshotId: snap.snapshotId } });\n```\n\nSnapshots 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.\n\n## Images\n\nPass `image` to control the environment. Managed images live under `vercel/sandbox`:\n\n| Image | Contents |\n|---|---|\n| `vercel/sandbox/universal` (default) | Ubuntu + Node.js 24, Python 3.14, coding agents, utilities |\n| `vercel/sandbox/node:22\\|24\\|26` | Ubuntu + pinned Node.js, pnpm |\n| `vercel/sandbox/python:3.14` | Ubuntu + Python 3.14, pip, venv, uv |\n| `vercel/sandbox/ubuntu` | Minimal Ubuntu 26.04 + sudo |\n| `vercel/sandbox/arch` | Arch Linux, yay, base-devel |\n\n**Custom images** (bake in your own tools) go through Vercel Container Registry: `vercel vcr build docker . my-repo:latest --push`, then `image: \"my-repo:latest\"`. Team-scoped (`team/project/repo:tag`) and public images work too. Note: Sandbox does **not** run a Dockerfile `ENTRYPOINT`/`CMD` — start processes yourself with `runCommand` after create. Pin a digest (`image@sha256:...`) for reproducibility.\n\n## Drives (beta)\n\nA drive is persistent storage you mount into a sandbox as a directory; unlike a snapshot (a full-filesystem copy per sandbox), a drive is one directory many sandboxes share and keep updating across runs. Good for agent workspaces, dependency caches, and shared data.\n\n```ts\nimport { Sandbox, Drive } from \"@vercel/sandbox\";\n\nconst drive = await Drive.getOrCreate({ name: \"workspace-cache\" });\nconst sandbox = await Sandbox.create({ mounts: { \"/data\": drive } }); // read-write\n\n// Concurrent read-only access via a drive snapshot\nconst reader = await Sandbox.create({ mounts: { \"/data\": drive.snapshot() } });\n```\n\nUp to 4 drives per run. Default size 1 TiB (1 GiB on Hobby), max 16 TiB. A drive lives in one region; a sandbox mounting it must use that region as its main region (failover regions still load the drive, with higher read latency). Only one sandbox at a time can mount a drive read-write; use `drive.snapshot()` for shared reads.\n\n## Network policy and credential brokering\n\nThe egress firewall is Sandbox's key security control for untrusted code. Set `networkPolicy` at create or via `sandbox.update({ networkPolicy })`:\n\n- `\"allow-all\"` (default) — all egress allowed.\n- `\"deny-all\"` — blocks all egress, including DNS. Start here for untrusted code.\n- Rule object — an `allow` list restricts egress to **only** the listed domains (everything else is denied); add `subnets.allow`/`subnets.deny` for IP ranges (`deny` wins). Domain matching is SNI-based, so non-TLS traffic is denied unless allowed by IP range (`subnets.allow`) or the policy includes a `*` catch-all (which lets domain-less traffic through); `subnets.deny` only removes access an allow rule granted.\n\n**Credential brokering**: a `transform` rule injects a secret header on egress to an allowed domain, so code inside the VM can call an authenticated API **without the secret ever entering the sandbox**. Because the `allow` list denies everything else, the box can reach only that one domain:\n\n```ts\nconst sandbox = await Sandbox.create({\n  networkPolicy: {\n    allow: {\n      \"api.example.com\": [{\n        transform: [{ headers: { Authorization: `Bearer ${process.env.API_SECRET}` } }],\n      }],\n    },\n  },\n});\n// Inside the VM: fetch(\"https://api.example.com/…\") is authenticated by the\n// firewall; the VM never holds API_SECRET and can't reach any other host\n// (no catch-all `*` rule, so non-TLS / domain-less egress is denied too).\n```\n\n## Running AI agents in a sandbox\n\nTo run AI-generated code, or a coding agent (Claude Code, Codex) that edits and executes code, put it in a sandbox. Two ways to give it model access, by trust level:\n\n**Trusted agent — inject the OIDC token, call the AI Gateway directly.** The AI Gateway accepts a Vercel OIDC token as a bearer credential, so no model API key is needed. The sandbox does **not** automatically have your `VERCEL_OIDC_TOKEN`, so pass it in via `env`:\n\n```ts\nconst sandbox = await Sandbox.create({\n  env: { VERCEL_OIDC_TOKEN: process.env.VERCEL_OIDC_TOKEN! },\n});\n// Inside the VM, hit the gateway (OpenAI-compatible at /v1, Anthropic-compatible at root):\n//   curl https://ai-gateway.vercel.sh/v1/chat/completions \\\n//     -H \"Authorization: Bearer $VERCEL_OIDC_TOKEN\" -H \"Content-Type: application/json\" \\\n//     -d '{\"model\":\"anthropic/claude-sonnet-5\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n// The AI SDK auto-resolves VERCEL_OIDC_TOKEN when AI_GATEWAY_API_KEY isn't set. Model ids\n// come from the AI Gateway model catalog (provider/model, e.g. \"anthropic/claude-sonnet-5\").\n```\n\n**Untrusted code — broker the credential, keep it out of the VM.** For code you don't trust, don't put the token in the VM at all. Allow only the gateway and inject the auth header at the firewall so the box holds no credential and can reach no other host (without a `*` catch-all, non-TLS egress is denied too):\n\n```ts\nconst sandbox = await Sandbox.create({\n  networkPolicy: {\n    allow: {\n      \"ai-gateway.vercel.sh\": [{\n        transform: [{ headers: { Authorization: `Bearer ${process.env.VERCEL_OIDC_TOKEN}` } }],\n      }],\n    },\n  },\n});\n// Agent code calls https://ai-gateway.vercel.sh with no token present in the VM.\n```\n\nThe OIDC token is ~12h; for longer sessions re-inject on resume or scope work to the token's life.\n\n## Multi-agent isolation\n\nRun several agents in one sandbox, each as its own Linux user with a private home directory (JS SDK only; image must include `/bin/bash`):\n\n```ts\nconst alice = await sandbox.createUser(\"alice\"); // /home/alice\nawait alice.runCommand(\"whoami\"); // runs as alice\nconst root = sandbox.asUser(\"root\");\n```\n\nFiles in one user's home are unreadable by another. Share a workspace with `sandbox.createGroup(\"team\")` (dir at `/shared/team`) and `addUserToGroup`.\n\n## CLI\n\nThe `sandbox` CLI (also `vercel sandbox`) mirrors the SDK, Docker-style:\n\n```bash\nsandbox create --name my-box              # create (persistent; --non-persistent to opt out)\nsandbox exec my-box -- npm test           # run a command in a named sandbox (resumes if stopped)\nsandbox run -- node --version             # create an ephemeral box, run once\nsandbox connect my-box                    # interactive shell (aliases: ssh, shell)\nsandbox copy ./local my-box:/remote       # copy files (alias: cp)\nsandbox list                              # list sandboxes (alias: ls)\nsandbox drives get-or-create cache        # create a drive\nsandbox stop my-box\n```\n\n## Limits and cost\n\n- **Session duration**: Hobby 45 min, Pro/Ent 24h (per session; resume for longer).\n- **Resources**: `vcpus` 1 or even up to 4/8/32 (Hobby/Pro/Ent), 2 GB RAM per vCPU, 15 ports, 64 GB disk.\n- **Concurrency**: Hobby 10, Pro/Ent 10,000 concurrent sandboxes.\n- **Network**: data your sandbox **downloads** (npm, git, datasets) is **free**; data it sends out and all exposed-port traffic is billable.\n- **Isolation**: each sandbox is a separate Firecracker microVM, so a crash, fork bomb, or disk-fill is contained to that VM. There are no per-process CPU/PID quotas inside the VM beyond the vCPU and 64 GB disk limits — cap risk with a short `timeout` and `deny-all` for untrusted code.\n- **Save money**: call `stop()` when done with one-off work, right-size vCPUs (down to 1), use `persistent: false` for scratch runs, and a smaller image or `keepLastSnapshots: { count: 1 }` to cut snapshot storage.\n\n## Best-practice checklist\n\n- For one-off work, `stop()` in a `finally` (safe to call more than once). **Exception**: a long-lived sandbox serving an exposed port — don't stop it, or the URL goes dead; leave it running (it persists/resumes).\n- `runCommand` has no shell (wrap pipes/redirects/`&&` in `bash -c`) and does not throw on non-zero exit (check `result.exitCode`); it rejects if the session times out mid-command.\n- Use absolute `/vercel/sandbox` paths (or an explicit `cwd`) when a command reads/writes files you created with `writeFiles`.\n- `apt-get update` before `apt-get install`; commands are non-root by default (`sudo: true` for root); `sudo`/`detached` are object-form only.\n- Start per-session services (dev servers) in **both** `onCreate` and `onResume` — only the filesystem survives a stop.\n- For untrusted code: `networkPolicy: \"deny-all\"` (or a tight allow-list), a short `timeout` (e.g. `30_000`), `vcpus: 1`, and `persistent: false`.\n- Exposed servers must bind `0.0.0.0`, not `127.0.0.1`.\n- Pin a custom image by digest for reproducible boots; `ENTRYPOINT`/`CMD` don't run.\n"},"changes":[{"path":"/skill_md_contents","type":"changed","before":"---\nname: vercel-sandbox\ndescription: 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.\nmetadata:\n  priority: 4\n  docs:\n    - \"https://vercel.com/docs/sandbox\"\n  sitemap: \"https://vercel.com/sitemap/docs.xml\"\n  pathPatterns: []\n  importPatterns:\n    - '@vercel/sandbox'\n  bashPatterns:\n    - '\\bnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\bpnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\bbun\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\byarn\\s+add\\s+[^\\n]*@vercel/sandbox\\b'\n  promptSignals:\n    phrases:\n      - \"@vercel/sandbox\"\n      - \"sandbox\"\n      - \"code sandbox\"\n      - \"vercel sandbox\"\n      - \"isolated environment\"\n      - \"sandboxed execution\"\n    allOf:\n      - [sandbox, code]\n      - [sandbox, execute]\n      - [sandbox, run]\n      - [sandbox, isolated]\n      - [sandbox, safe]\n      - [sandbox, environment]\n      - [isolated, execute]\n      - [isolated, code]\n      - [isolated, environment]\n      - [isolated, run]\n      - [safe, execute]\n      - [safe, code]\n      - [untrusted, code]\n      - [untrusted, execute]\n      - [code, runner]\n      - [code, playground]\n      - [execute, safely]\n      - [run, safely]\n      - [run, isolation]\n      - [execute, isolation]\n      - [ffmpeg, process]\n      - [ffmpeg, convert]\n      - [ffmpeg, compress]\n      - [student, code]\n      - [student, execute]\n      - [student, run]\n    anyOf:\n      - \"sandbox\"\n      - \"isolated\"\n      - \"isolation\"\n      - \"untrusted\"\n      - \"safely\"\n      - \"microvm\"\n      - \"ffmpeg\"\n      - \"playground\"\n    noneOf:\n      - \"iframe sandbox\"\n      - \"sandbox attribute\"\n      - \"codesandbox.io\"\n      - \"stackblitz\"\n    minScore: 4\n---\n\n# Browser Automation with Vercel Sandbox\n\nRun agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs. A Linux VM spins up on demand, executes browser commands, and shuts down. Works with any Vercel-deployed framework (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.).\n\n## Dependencies\n\n```bash\npnpm add @vercel/sandbox\n```\n\nThe sandbox VM needs system dependencies for Chromium plus agent-browser itself. Use sandbox snapshots (below) to pre-install everything for sub-second startup.\n\n## Core Pattern\n\n```ts\nimport { Sandbox } from \"@vercel/sandbox\";\n\n// System libraries required by Chromium on the sandbox VM (Amazon Linux / dnf)\nconst CHROMIUM_SYSTEM_DEPS = [\n  \"nss\", \"nspr\", \"libxkbcommon\", \"atk\", \"at-spi2-atk\", \"at-spi2-core\",\n  \"libXcomposite\", \"libXdamage\", \"libXrandr\", \"libXfixes\", \"libXcursor\",\n  \"libXi\", \"libXtst\", \"libXScrnSaver\", \"libXext\", \"mesa-libgbm\", \"libdrm\",\n  \"mesa-libGL\", \"mesa-libEGL\", \"cups-libs\", \"alsa-lib\", \"pango\", \"cairo\",\n  \"gtk3\", \"dbus-libs\",\n];\n\nfunction getSandboxCredentials() {\n  if (\n    process.env.VERCEL_TOKEN &&\n    process.env.VERCEL_TEAM_ID &&\n    process.env.VERCEL_PROJECT_ID\n  ) {\n    return {\n      token: process.env.VERCEL_TOKEN,\n      teamId: process.env.VERCEL_TEAM_ID,\n      projectId: process.env.VERCEL_PROJECT_ID,\n    };\n  }\n  return {};\n}\n\nasync function withBrowser<T>(\n  fn: (sandbox: InstanceType<typeof Sandbox>) => Promise<T>,\n): Promise<T> {\n  const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID;\n  const credentials = getSandboxCredentials();\n\n  const sandbox = snapshotId\n    ? await Sandbox.create({\n        ...credentials,\n        source: { type: \"snapshot\", snapshotId },\n        timeout: 120_000,\n      })\n    : await Sandbox.create({ ...credentials, runtime: \"node24\", timeout: 120_000 });\n\n  if (!snapshotId) {\n    await sandbox.runCommand(\"sh\", [\n      \"-c\",\n      `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(\" \")} 2>&1 && sudo ldconfig 2>&1`,\n    ]);\n    await sandbox.runCommand(\"npm\", [\"install\", \"-g\", \"agent-browser\"]);\n    await sandbox.runCommand(\"npx\", [\"agent-browser\", \"install\"]);\n  }\n\n  try {\n    return await fn(sandbox);\n  } finally {\n    await sandbox.stop();\n  }\n}\n```\n\n## Screenshot\n\nThe `screenshot --json` command saves to a file and returns the path. Read the file back as base64:\n\n```ts\nexport async function screenshotUrl(url: string) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const titleResult = await sandbox.runCommand(\"agent-browser\", [\n      \"get\", \"title\", \"--json\",\n    ]);\n    const title = JSON.parse(await titleResult.stdout())?.data?.title || url;\n\n    const ssResult = await sandbox.runCommand(\"agent-browser\", [\n      \"screenshot\", \"--json\",\n    ]);\n    const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;\n    const b64Result = await sandbox.runCommand(\"base64\", [\"-w\", \"0\", ssPath]);\n    const screenshot = (await b64Result.stdout()).trim();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n\n    return { title, screenshot };\n  });\n}\n```\n\n## Accessibility Snapshot\n\n```ts\nexport async function snapshotUrl(url: string) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const titleResult = await sandbox.runCommand(\"agent-browser\", [\n      \"get\", \"title\", \"--json\",\n    ]);\n    const title = JSON.parse(await titleResult.stdout())?.data?.title || url;\n\n    const snapResult = await sandbox.runCommand(\"agent-browser\", [\n      \"snapshot\", \"-i\", \"-c\",\n    ]);\n    const snapshot = await snapResult.stdout();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n\n    return { title, snapshot };\n  });\n}\n```\n\n## Multi-Step Workflows\n\nThe sandbox persists between commands, so you can run full automation sequences:\n\n```ts\nexport async function fillAndSubmitForm(url: string, data: Record<string, string>) {\n  return withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", url]);\n\n    const snapResult = await sandbox.runCommand(\"agent-browser\", [\n      \"snapshot\", \"-i\",\n    ]);\n    const snapshot = await snapResult.stdout();\n    // Parse snapshot to find element refs...\n\n    for (const [ref, value] of Object.entries(data)) {\n      await sandbox.runCommand(\"agent-browser\", [\"fill\", ref, value]);\n    }\n\n    await sandbox.runCommand(\"agent-browser\", [\"click\", \"@e5\"]);\n    await sandbox.runCommand(\"agent-browser\", [\"wait\", \"--load\", \"networkidle\"]);\n\n    const ssResult = await sandbox.runCommand(\"agent-browser\", [\n      \"screenshot\", \"--json\",\n    ]);\n    const ssPath = JSON.parse(await ssResult.stdout())?.data?.path;\n    const b64Result = await sandbox.runCommand(\"base64\", [\"-w\", \"0\", ssPath]);\n    const screenshot = (await b64Result.stdout()).trim();\n\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n\n    return { screenshot };\n  });\n}\n```\n\n## Sandbox Snapshots (Fast Startup)\n\nA **sandbox snapshot** is a saved VM image of a Vercel Sandbox with system dependencies + agent-browser + Chromium already installed. Think of it like a Docker image -- instead of installing dependencies from scratch every time, the sandbox boots from the pre-built image.\n\nThis is unrelated to agent-browser's *accessibility snapshot* feature (`agent-browser snapshot`), which dumps a page's accessibility tree. A sandbox snapshot is a Vercel infrastructure concept for fast VM startup.\n\nWithout a sandbox snapshot, each run installs system deps + agent-browser + Chromium (~30s). With one, startup is sub-second.\n\n### Creating a sandbox snapshot\n\nThe snapshot must include system dependencies (via `dnf`), agent-browser, and Chromium:\n\n```ts\nimport { Sandbox } from \"@vercel/sandbox\";\n\nconst CHROMIUM_SYSTEM_DEPS = [\n  \"nss\", \"nspr\", \"libxkbcommon\", \"atk\", \"at-spi2-atk\", \"at-spi2-core\",\n  \"libXcomposite\", \"libXdamage\", \"libXrandr\", \"libXfixes\", \"libXcursor\",\n  \"libXi\", \"libXtst\", \"libXScrnSaver\", \"libXext\", \"mesa-libgbm\", \"libdrm\",\n  \"mesa-libGL\", \"mesa-libEGL\", \"cups-libs\", \"alsa-lib\", \"pango\", \"cairo\",\n  \"gtk3\", \"dbus-libs\",\n];\n\nasync function createSnapshot(): Promise<string> {\n  const sandbox = await Sandbox.create({\n    runtime: \"node24\",\n    timeout: 300_000,\n  });\n\n  await sandbox.runCommand(\"sh\", [\n    \"-c\",\n    `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(\" \")} 2>&1 && sudo ldconfig 2>&1`,\n  ]);\n  await sandbox.runCommand(\"npm\", [\"install\", \"-g\", \"agent-browser\"]);\n  await sandbox.runCommand(\"npx\", [\"agent-browser\", \"install\"]);\n\n  const snapshot = await sandbox.snapshot();\n  return snapshot.snapshotId;\n}\n```\n\nRun this once, then set the environment variable:\n\n```bash\nAGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx\n```\n\nA helper script is available in the demo app:\n\n```bash\nnpx tsx examples/environments/scripts/create-snapshot.ts\n```\n\nRecommended for any production deployment using the Sandbox pattern.\n\n## Authentication\n\nOn Vercel deployments, the Sandbox SDK authenticates automatically via OIDC. For local development or explicit control, set:\n\n```bash\nVERCEL_TOKEN=<personal-access-token>\nVERCEL_TEAM_ID=<team-id>\nVERCEL_PROJECT_ID=<project-id>\n```\n\nThese are spread into `Sandbox.create()` calls. When absent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel).\n\n## Scheduled Workflows (Cron)\n\nCombine with Vercel Cron Jobs for recurring browser tasks:\n\n```ts\n// app/api/cron/route.ts  (or equivalent in your framework)\nexport async function GET() {\n  const result = await withBrowser(async (sandbox) => {\n    await sandbox.runCommand(\"agent-browser\", [\"open\", \"https://example.com/pricing\"]);\n    const snap = await sandbox.runCommand(\"agent-browser\", [\"snapshot\", \"-i\", \"-c\"]);\n    await sandbox.runCommand(\"agent-browser\", [\"close\"]);\n    return await snap.stdout();\n  });\n\n  // Process results, send alerts, store data...\n  return Response.json({ ok: true, snapshot: result });\n}\n```\n\n```json\n// vercel.json\n{ \"crons\": [{ \"path\": \"/api/cron\", \"schedule\": \"0 9 * * *\" }] }\n```\n\n## Environment Variables\n\n| Variable | Required | Description |\n|---|---|---|\n| `AGENT_BROWSER_SNAPSHOT_ID` | No (but recommended) | Pre-built sandbox snapshot ID for sub-second startup (see above) |\n| `VERCEL_TOKEN` | No | Vercel personal access token (for local dev; OIDC is automatic on Vercel) |\n| `VERCEL_TEAM_ID` | No | Vercel team ID (for local dev) |\n| `VERCEL_PROJECT_ID` | No | Vercel project ID (for local dev) |\n\n## Framework Examples\n\nThe pattern works identically across frameworks. The only difference is where you put the server-side code:\n\n| Framework | Server code location |\n|---|---|\n| Next.js | Server actions, API routes, route handlers |\n| SvelteKit | `+page.server.ts`, `+server.ts` |\n| Nuxt | `server/api/`, `server/routes/` |\n| Remix | `loader`, `action` functions |\n| Astro | `.astro` frontmatter, API routes |\n\n## Example\n\nSee `examples/environments/` in the agent-browser repo for a working app with the Vercel Sandbox pattern, including a sandbox snapshot creation script, streaming progress UI, and rate limiting.\n","after":"---\nname: vercel-sandbox\ndescription: 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.\nsummary: \"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\"\nmetadata:\n  priority: 4\n  docs:\n    - \"https://vercel.com/docs/sandbox\"\n  sitemap: \"https://vercel.com/sitemap.xml\"\n  pathPatterns: []\n  importPatterns:\n    - '@vercel/sandbox'\n  bashPatterns:\n    - '\\bnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\bpnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\bbun\\s+(install|i|add)\\s+[^\\n]*@vercel/sandbox\\b'\n    - '\\byarn\\s+add\\s+[^\\n]*@vercel/sandbox\\b'\n  promptSignals:\n    phrases:\n      - \"@vercel/sandbox\"\n      - \"sandbox\"\n      - \"code sandbox\"\n      - \"vercel sandbox\"\n      - \"isolated environment\"\n      - \"sandboxed execution\"\n    allOf:\n      - [sandbox, code]\n      - [sandbox, execute]\n      - [sandbox, run]\n      - [sandbox, isolated]\n      - [sandbox, safe]\n      - [sandbox, environment]\n      - [isolated, execute]\n      - [isolated, code]\n      - [isolated, environment]\n      - [isolated, run]\n      - [safe, execute]\n      - [safe, code]\n      - [untrusted, code]\n      - [untrusted, execute]\n      - [code, runner]\n      - [code, playground]\n      - [execute, safely]\n      - [run, safely]\n      - [run, isolation]\n      - [execute, isolation]\n      - [ffmpeg, process]\n      - [ffmpeg, convert]\n      - [ffmpeg, compress]\n      - [student, code]\n      - [student, execute]\n      - [student, run]\n    anyOf:\n      - \"sandbox\"\n      - \"isolated\"\n      - \"isolation\"\n      - \"untrusted\"\n      - \"safely\"\n      - \"microvm\"\n      - \"ffmpeg\"\n      - \"playground\"\n    noneOf:\n      - \"iframe sandbox\"\n      - \"sandbox attribute\"\n      - \"codesandbox.io\"\n      - \"stackblitz\"\n    minScore: 4\nretrieval:\n  aliases:\n    - code sandbox\n    - microvm\n    - isolated execution\n    - safe code runner\n  intents:\n    - run untrusted code\n    - execute code safely\n    - create sandbox\n    - isolate code execution\n  entities:\n    - Vercel Sandbox\n    - Firecracker\n    - microVM\n    - isolated execution\nchainTo:\n  -\n    pattern: 'from\\s+[''\"\"]vm2[''\"\"]|require\\s*\\(\\s*[''\"\"]vm2[''\"\"\\)]|new\\s+VM\\('\n    targetSkill: vercel-sandbox\n    message: 'vm2 detected — it has known security vulnerabilities. Reloading Vercel Sandbox guidance for Firecracker microVM-based safe execution.'\n  -\n    pattern: 'child_process.*exec\\(|execSync\\(|spawn\\(.*\\{.*shell:\\s*true'\n    targetSkill: ai-sdk\n    message: 'Shell exec for code execution detected — loading AI SDK guidance for tool-calling patterns that pair with Vercel Sandbox for safe agent execution.'\n\n---\n\n# Vercel Sandbox\n\nVercel 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.\n\nDo **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.\n\n## Install\n\n```bash\npnpm add @vercel/sandbox   # or npm i / yarn add / bun add\n```\n\nThere is also a Python SDK (`vercel` package, `vercel.sandbox`) and a `sandbox` CLI. This skill shows the JS SDK unless noted.\n\n## Minimal example\n\nThe 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.\n\n```ts\nimport { Sandbox } from \"@vercel/sandbox\";\n\nconst sandbox = await Sandbox.create();\ntry {\n  const result = await sandbox.runCommand(\"python3\", [\"-c\", \"print(2 + 2)\"]);\n  console.log(await result.stdout()); // \"4\\n\"\n  console.log(result.exitCode);       // 0\n} finally {\n  await sandbox.stop();\n}\n```\n\n`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.\n\n## Authentication\n\n- **On Vercel** (Functions, Cron, builds): the SDK authenticates automatically via the deployment's OIDC token. No config.\n- **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).\n- **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).\n\nThis 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)).\n\n## Creating a sandbox\n\nCommon `Sandbox.create()` options (all optional):\n\n| Option | Default | Notes |\n|---|---|---|\n| `image` | `vercel/sandbox/universal` | Managed image, or a custom/public VCR image. See [Images](#images). |\n| `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. |\n| `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). |\n| `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. |\n| `region` | project default or `iad1` | One of 19 regions. |\n| `persistent` | `true` | Auto-snapshots on stop and resumes on next call. Pass `false` for one-off work to avoid snapshot storage cost. |\n| `networkPolicy` | `\"allow-all\"` | Use `\"deny-all\"` or an allow-list for untrusted code. See [Network policy](#network-policy-and-credential-brokering). |\n| `env` | – | Environment variables for every command. Per-command `env` overrides these. Use this to inject a credential into the VM. |\n| `name` | auto-generated | Unique per project, immutable. Used to retrieve/resume a persistent sandbox. |\n| `tags` | – | Up to 5 key-value pairs for filtering in `Sandbox.list()`. |\n\n## Running commands\n\n`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.\n\nCommands run as a **non-root** user (`ubuntu`, in the sudo group) by default; pass `sudo: true` for root.\n\n```ts\nconst r = await sandbox.runCommand(\"npm\", [\"install\"]);\nif (r.exitCode !== 0) throw new Error(await r.stderr()); // non-zero does NOT throw\n\n// Needs a shell for the redirect / pipe. Use absolute paths (see below).\nawait sandbox.runCommand(\"bash\", [\"-c\", \"echo hi > /vercel/sandbox/out.txt && cat /vercel/sandbox/out.txt\"]);\n\n// Root for one command (sudo is object-form only)\nawait sandbox.runCommand({ cmd: \"apt-get\", args: [\"update\"], sudo: true });\n\n// Long-running process: detached (object form only) returns immediately\nconst server = await sandbox.runCommand({ cmd: \"npm\", args: [\"run\", \"dev\"], detached: true });\n```\n\n`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.\n\n**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.\n\n## Files\n\nFile-method paths are relative to `/vercel/sandbox` unless absolute. `content` must be a `Buffer`.\n\n```ts\nawait sandbox.writeFiles([\n  { path: \"input.txt\", content: Buffer.from(\"line one\\nline two\\n\") },\n  { path: \"run.sh\", content: Buffer.from(\"#!/bin/bash\\necho hi\"), mode: 0o755 },\n]);\n\n// readFileToBuffer returns a Buffer, or null if the file is missing — guard it.\n// (readFile returns a ReadableStream; neither returns a string, so convert yourself.)\nconst buf = await sandbox.readFileToBuffer({ path: \"input.txt\" });\nconst text = buf?.toString(\"utf8\") ?? \"\";\n\nawait sandbox.mkDir(\"src/generated\");\n```\n\nTo 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 }`).\n\n## Installing system packages\n\nThe 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:\n\n```ts\nconst install = await sandbox.runCommand({\n  cmd: \"bash\",\n  args: [\"-c\", \"apt-get update && apt-get install -y ffmpeg\"],\n  sudo: true,\n});\nif (install.exitCode !== 0) throw new Error(await install.stderr());\n```\n\nFor 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.\n\n## Ports and preview URLs\n\nExpose 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:\n\n```ts\nconst sandbox = await Sandbox.create({ ports: [3000] });\n// Bind 0.0.0.0 — e.g. Next/Vite: `run dev -- --host 0.0.0.0`; node http: listen(\"0.0.0.0\")\nawait sandbox.runCommand({ cmd: \"npm\", args: [\"run\", \"dev\", \"--\", \"--host\", \"0.0.0.0\"], detached: true });\n\n// Wait until the port actually answers inside the VM\nfor (let i = 0; i < 30; i++) {\n  const ping = await sandbox.runCommand(\"bash\", [\"-c\", \"curl -sf http://localhost:3000 >/dev/null && echo up || true\"]);\n  if ((await ping.stdout()).includes(\"up\")) break;\n  await new Promise((r) => setTimeout(r, 1000));\n}\nconst url = sandbox.domain(3000); // public HTTPS URL for port 3000\n```\n\nThe 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).\n\n## Lifecycle and persistence\n\n**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).\n\n- **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.\n- **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.\n- **`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).\n- **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.\n- 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)).\n\n```ts\nconst startDev = (s) =>\n  s.runCommand({ cmd: \"npm\", args: [\"run\", \"dev\"], detached: true, cwd: \"/vercel/sandbox\" });\n\nconst sandbox = await Sandbox.getOrCreate({\n  name: \"agent-ws\",\n  ports: [3000],\n  onCreate: async (s) => {          // once, on first creation\n    await s.runCommand({ cmd: \"git\", args: [\"clone\", repoUrl, \".\"], cwd: \"/vercel/sandbox\" });\n    await s.runCommand({ cmd: \"npm\", args: [\"install\"], cwd: \"/vercel/sandbox\" });\n    await startDev(s);              // up on first boot, before domain() is read\n  },\n  onResume: async (s) => startDev(s), // every later resume\n});\n// Poll for the server to be listening (see Ports) before using the URL.\nconst url = sandbox.domain(3000);\n// Don't stop this sandbox in a finally — stopping kills the dev server and the URL.\n// If this process might find the sandbox already existing (not freshly created), pass\n// `resume: true` above and start the server yourself — the hooks only fire on create/this call.\n```\n\nA separate later process reconnects by name and resumes on the first command. It won't run the hooks above, so restart anything it needs:\n\n```ts\nconst sandbox = await Sandbox.get({ name: \"agent-ws\" });\nconst test = await sandbox.runCommand({ cmd: \"npm\", args: [\"test\"], cwd: \"/vercel/sandbox\" }); // resumes, then runs\n```\n\nOpt 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.\n\n## Snapshots\n\nA 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).\n\n```ts\nconst running = await Sandbox.create({ image: \"vercel/sandbox/node:24\" });\nawait running.runCommand({ cmd: \"bash\", args: [\"-c\", \"apt-get update && apt-get install -y ffmpeg\"], sudo: true });\nconst snap = await running.snapshot(); // sandbox stops automatically after; do NOT call stop()\n\nconst fast = await Sandbox.create({ source: { type: \"snapshot\", snapshotId: snap.snapshotId } });\n```\n\nSnapshots 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.\n\n## Images\n\nPass `image` to control the environment. Managed images live under `vercel/sandbox`:\n\n| Image | Contents |\n|---|---|\n| `vercel/sandbox/universal` (default) | Ubuntu + Node.js 24, Python 3.14, coding agents, utilities |\n| `vercel/sandbox/node:22\\|24\\|26` | Ubuntu + pinned Node.js, pnpm |\n| `vercel/sandbox/python:3.14` | Ubuntu + Python 3.14, pip, venv, uv |\n| `vercel/sandbox/ubuntu` | Minimal Ubuntu 26.04 + sudo |\n| `vercel/sandbox/arch` | Arch Linux, yay, base-devel |\n\n**Custom images** (bake in your own tools) go through Vercel Container Registry: `vercel vcr build docker . my-repo:latest --push`, then `image: \"my-repo:latest\"`. Team-scoped (`team/project/repo:tag`) and public images work too. Note: Sandbox does **not** run a Dockerfile `ENTRYPOINT`/`CMD` — start processes yourself with `runCommand` after create. Pin a digest (`image@sha256:...`) for reproducibility.\n\n## Drives (beta)\n\nA drive is persistent storage you mount into a sandbox as a directory; unlike a snapshot (a full-filesystem copy per sandbox), a drive is one directory many sandboxes share and keep updating across runs. Good for agent workspaces, dependency caches, and shared data.\n\n```ts\nimport { Sandbox, Drive } from \"@vercel/sandbox\";\n\nconst drive = await Drive.getOrCreate({ name: \"workspace-cache\" });\nconst sandbox = await Sandbox.create({ mounts: { \"/data\": drive } }); // read-write\n\n// Concurrent read-only access via a drive snapshot\nconst reader = await Sandbox.create({ mounts: { \"/data\": drive.snapshot() } });\n```\n\nUp to 4 drives per run. Default size 1 TiB (1 GiB on Hobby), max 16 TiB. A drive lives in one region; a sandbox mounting it must use that region as its main region (failover regions still load the drive, with higher read latency). Only one sandbox at a time can mount a drive read-write; use `drive.snapshot()` for shared reads.\n\n## Network policy and credential brokering\n\nThe egress firewall is Sandbox's key security control for untrusted code. Set `networkPolicy` at create or via `sandbox.update({ networkPolicy })`:\n\n- `\"allow-all\"` (default) — all egress allowed.\n- `\"deny-all\"` — blocks all egress, including DNS. Start here for untrusted code.\n- Rule object — an `allow` list restricts egress to **only** the listed domains (everything else is denied); add `subnets.allow`/`subnets.deny` for IP ranges (`deny` wins). Domain matching is SNI-based, so non-TLS traffic is denied unless allowed by IP range (`subnets.allow`) or the policy includes a `*` catch-all (which lets domain-less traffic through); `subnets.deny` only removes access an allow rule granted.\n\n**Credential brokering**: a `transform` rule injects a secret header on egress to an allowed domain, so code inside the VM can call an authenticated API **without the secret ever entering the sandbox**. Because the `allow` list denies everything else, the box can reach only that one domain:\n\n```ts\nconst sandbox = await Sandbox.create({\n  networkPolicy: {\n    allow: {\n      \"api.example.com\": [{\n        transform: [{ headers: { Authorization: `Bearer ${process.env.API_SECRET}` } }],\n      }],\n    },\n  },\n});\n// Inside the VM: fetch(\"https://api.example.com/…\") is authenticated by the\n// firewall; the VM never holds API_SECRET and can't reach any other host\n// (no catch-all `*` rule, so non-TLS / domain-less egress is denied too).\n```\n\n## Running AI agents in a sandbox\n\nTo run AI-generated code, or a coding agent (Claude Code, Codex) that edits and executes code, put it in a sandbox. Two ways to give it model access, by trust level:\n\n**Trusted agent — inject the OIDC token, call the AI Gateway directly.** The AI Gateway accepts a Vercel OIDC token as a bearer credential, so no model API key is needed. The sandbox does **not** automatically have your `VERCEL_OIDC_TOKEN`, so pass it in via `env`:\n\n```ts\nconst sandbox = await Sandbox.create({\n  env: { VERCEL_OIDC_TOKEN: process.env.VERCEL_OIDC_TOKEN! },\n});\n// Inside the VM, hit the gateway (OpenAI-compatible at /v1, Anthropic-compatible at root):\n//   curl https://ai-gateway.vercel.sh/v1/chat/completions \\\n//     -H \"Authorization: Bearer $VERCEL_OIDC_TOKEN\" -H \"Content-Type: application/json\" \\\n//     -d '{\"model\":\"anthropic/claude-sonnet-5\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'\n// The AI SDK auto-resolves VERCEL_OIDC_TOKEN when AI_GATEWAY_API_KEY isn't set. Model ids\n// come from the AI Gateway model catalog (provider/model, e.g. \"anthropic/claude-sonnet-5\").\n```\n\n**Untrusted code — broker the credential, keep it out of the VM.** For code you don't trust, don't put the token in the VM at all. Allow only the gateway and inject the auth header at the firewall so the box holds no credential and can reach no other host (without a `*` catch-all, non-TLS egress is denied too):\n\n```ts\nconst sandbox = await Sandbox.create({\n  networkPolicy: {\n    allow: {\n      \"ai-gateway.vercel.sh\": [{\n        transform: [{ headers: { Authorization: `Bearer ${process.env.VERCEL_OIDC_TOKEN}` } }],\n      }],\n    },\n  },\n});\n// Agent code calls https://ai-gateway.vercel.sh with no token present in the VM.\n```\n\nThe OIDC token is ~12h; for longer sessions re-inject on resume or scope work to the token's life.\n\n## Multi-agent isolation\n\nRun several agents in one sandbox, each as its own Linux user with a private home directory (JS SDK only; image must include `/bin/bash`):\n\n```ts\nconst alice = await sandbox.createUser(\"alice\"); // /home/alice\nawait alice.runCommand(\"whoami\"); // runs as alice\nconst root = sandbox.asUser(\"root\");\n```\n\nFiles in one user's home are unreadable by another. Share a workspace with `sandbox.createGroup(\"team\")` (dir at `/shared/team`) and `addUserToGroup`.\n\n## CLI\n\nThe `sandbox` CLI (also `vercel sandbox`) mirrors the SDK, Docker-style:\n\n```bash\nsandbox create --name my-box              # create (persistent; --non-persistent to opt out)\nsandbox exec my-box -- npm test           # run a command in a named sandbox (resumes if stopped)\nsandbox run -- node --version             # create an ephemeral box, run once\nsandbox connect my-box                    # interactive shell (aliases: ssh, shell)\nsandbox copy ./local my-box:/remote       # copy files (alias: cp)\nsandbox list                              # list sandboxes (alias: ls)\nsandbox drives get-or-create cache        # create a drive\nsandbox stop my-box\n```\n\n## Limits and cost\n\n- **Session duration**: Hobby 45 min, Pro/Ent 24h (per session; resume for longer).\n- **Resources**: `vcpus` 1 or even up to 4/8/32 (Hobby/Pro/Ent), 2 GB RAM per vCPU, 15 ports, 64 GB disk.\n- **Concurrency**: Hobby 10, Pro/Ent 10,000 concurrent sandboxes.\n- **Network**: data your sandbox **downloads** (npm, git, datasets) is **free**; data it sends out and all exposed-port traffic is billable.\n- **Isolation**: each sandbox is a separate Firecracker microVM, so a crash, fork bomb, or disk-fill is contained to that VM. There are no per-process CPU/PID quotas inside the VM beyond the vCPU and 64 GB disk limits — cap risk with a short `timeout` and `deny-all` for untrusted code.\n- **Save money**: call `stop()` when done with one-off work, right-size vCPUs (down to 1), use `persistent: false` for scratch runs, and a smaller image or `keepLastSnapshots: { count: 1 }` to cut snapshot storage.\n\n## Best-practice checklist\n\n- For one-off work, `stop()` in a `finally` (safe to call more than once). **Exception**: a long-lived sandbox serving an exposed port — don't stop it, or the URL goes dead; leave it running (it persists/resumes).\n- `runCommand` has no shell (wrap pipes/redirects/`&&` in `bash -c`) and does not throw on non-zero exit (check `result.exitCode`); it rejects if the session times out mid-command.\n- Use absolute `/vercel/sandbox` paths (or an explicit `cwd`) when a command reads/writes files you created with `writeFiles`.\n- `apt-get update` before `apt-get install`; commands are non-root by default (`sudo: true` for root); `sudo`/`detached` are object-form only.\n- Start per-session services (dev servers) in **both** `onCreate` and `onResume` — only the filesystem survives a stop.\n- For untrusted code: `networkPolicy: \"deny-all\"` (or a tight allow-list), a short `timeout` (e.g. `30_000`), `vcpus: 1`, and `persistent: false`.\n- Exposed servers must bind `0.0.0.0`, not `127.0.0.1`.\n- Pin a custom image by digest for reproducible boots; `ENTRYPOINT`/`CMD` don't run.\n"}],"summary":"Fields changed: 1. /skill_md_contents.","summary_kind":"deterministic","summary_metadata":{}}