npx skills add ...
npx skills add vercel-labs/vercel-plugin --skill 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.
This repo is now called vercel/vercel-plugin. Both names install the same content, but the install count here only covers this one.
npx skills add vercel-labs/vercel-plugin --skill 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.
There is also a Python SDK (vercel package, vercel.sandbox) and a sandbox CLI. This skill shows the JS SDK unless noted.
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.
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.
vercel link then vercel env pull to get a VERCEL_OIDC_TOKEN in .env.local (valid ~12h; re-pull when it expires).VERCEL_TOKEN, VERCEL_TEAM_ID, VERCEL_PROJECT_ID. The SDK picks these up automatically.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).
Common Sandbox.create() options (all optional):
| Option | Default | Notes |
|---|---|---|
image | vercel/sandbox/universal | Managed image, or a custom/public VCR image. See 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. |
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(). |
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.
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.
File-method paths are relative to /vercel/sandbox unless absolute. content must be a Buffer.
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 }).
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:
For a different base, use a managed image (vercel/sandbox/arch uses pacman/yay) or build a custom image so packages are baked in and there's nothing to install at runtime.
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:
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).
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).
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.Sandbox.get({ name }) returns the handle immediately and auto-resumes on the next call that needs a running VM (pass resume: false to disable). 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).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.onCreate for the first boot and onResume for later ones) poll until the port answers before treating domain(port) as live (see Ports).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:
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.
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).
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.
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 |
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.
A 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.
Up 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 run in that region and can't use failover regions. Only one sandbox at a time can mount a drive read-write; use drive.snapshot() for shared reads.
The egress firewall is Sandbox's key security control for untrusted code. Set networkPolicy at create or via sandbox.update({ networkPolicy }):
"allow-all" (default) — all egress allowed."deny-all" — blocks all egress, including DNS. Start here for untrusted code.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 it only applies to TLS traffic — pair with deny-all/subnet rules if non-TLS egress must be blocked too.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:
To 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:
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:
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 TLS host (add subnets.deny to block non-TLS egress too):
The OIDC token is ~12h; for longer sessions re-inject on resume or scope work to the token's life.
Run several agents in one sandbox, each as its own Linux user with a private home directory (JS SDK only; image must include /bin/bash):
Files in one user's home are unreadable by another. Share a workspace with sandbox.createGroup("team") (dir at /shared/team) and addUserToGroup.
The sandbox CLI (also vercel sandbox) mirrors the SDK, Docker-style:
vcpus 1 or even up to 4/8/32 (Hobby/Pro/Ent), 2 GB RAM per vCPU, 15 ports, 64 GB disk.timeout and deny-all for untrusted code.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.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).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./vercel/sandbox paths (or an explicit cwd) when a command reads/writes files you created with writeFiles.apt-get update before apt-get install; commands are non-root by default (sudo: true for root); sudo/detached are object-form only.onCreate and onResume — only the filesystem survives a stop.networkPolicy: "deny-all" (or a tight allow-list), a short timeout (e.g. 30_000), vcpus: 1, and persistent: false.0.0.0.0, not 127.0.0.1.ENTRYPOINT/CMD don't run.