plugins/vercel-sandbox/agent/skills/sandbox/SKILL.md
Creates isolated Linux MicroVMs using Vercel Sandbox SDK. Use when building code execution environments, running untrusted code, spinning up dev servers, testing in isolation, or when the user mentions "sandbox", "microvm", "isolated execution", or "@vercel/sandbox".
npx skillsauth add pleaseai/claude-code-plugins plugins/vercel-sandbox/agent/skills/sandboxInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
@vercel/sandbox DocumentationYour knowledge of @vercel/sandbox may be outdated.
Follow these instructions before starting on any sandbox-related tasks:
The @vercel/sandbox@2 SDK and sandbox@3 CLI replace anonymous, ephemeral
sandboxes with named, persistent sandboxes. Key differences from v1:
name (not sandboxId). Names are unique per project.runCommand, writeFiles, etc. automatically resume a stopped sandbox.Sandbox.getOrCreate, Sandbox.fork, sandbox.update,
sandbox.delete, sandbox.listSessions, sandbox.listSnapshots,
Snapshot.tree, defineSandboxProxy.name, persistent, tags, onResume, snapshotExpiration,
keepLastSnapshots, L7 network policy matchers, forwardURL.since/until.Sandbox.list({ since, until }) → Sandbox.list({ cursor, namePrefix, sortBy, tags }).name
instead of sandboxId.Essential imports:
// Core SDK
import {
Sandbox,
Session,
Snapshot,
Command,
CommandFinished,
} from "@vercel/sandbox";
import { APIError, StreamError } from "@vercel/sandbox";
// For advanced network policy with credential brokering and L7 matchers
import type {
NetworkPolicy,
NetworkPolicyRule,
NetworkTransformer,
} from "@vercel/sandbox";
// For implementing a request-forwarding proxy (forwardURL)
import { defineSandboxProxy } from "@vercel/sandbox/proxy";
// For timeouts
import ms from "ms"; // e.g., ms("5m"), ms("1h")
Available runtimes:
type RUNTIMES = "node26" | "node24" | "node22" | "python3.13";
import { Sandbox } from "@vercel/sandbox";
const sandbox = await Sandbox.create({
name: "my-dev-env", // Optional, random if omitted. Unique per project.
runtime: "node24",
resources: { vcpus: 4 }, // 2048 MB RAM per vCPU
ports: [3000], // Expose up to 15 ports
timeout: ms("10m"), // Default: 5 minutes
env: { NODE_ENV: "production" }, // Env vars inherited by all commands
tags: { env: "staging", team: "infra" }, // Up to 5 key:value tags
persistent: true, // Default: true. Auto-snapshots on stop, restores on resume.
snapshotExpiration: ms("7d"), // Default TTL for snapshots. Use 0 for no expiration.
});
console.log(sandbox.name);
// Retrieve by name. The sandbox will resume automatically the next time
// you run a command.
const sandbox = await Sandbox.get({ name: "my-dev-env" });
Sandbox.getOrCreate is the recommended pattern for long-lived sandboxes.
const sandbox = await Sandbox.getOrCreate({
name: "my-workspace",
runtime: "node24",
// Runs only the first time the sandbox is created.
onCreate: async (sbx) => {
await sbx.writeFiles([
{ path: "README.md", content: Buffer.from("# Hello") },
]);
await sbx.runCommand("npm", ["install"]);
},
// Runs every time the sandbox session is resumed (including after auto-resume).
onResume: async (sbx) => {
await sbx.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true });
},
});
Behavior:
name exists → resumes it and fires onResume.onCreate.onCreate.Use onResume to restart background services or rehydrate caches whenever a
persistent sandbox's session is resumed:
const sandbox = await Sandbox.get({
name: "my-workspace",
onResume: async (sbx) => {
await sbx.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true });
},
});
onResume also fires when a SDK call (e.g. runCommand, writeFiles)
auto-resumes a stopped sandbox.
const sandbox = await Sandbox.create({
source: {
type: "git",
url: "https://github.com/vercel/sandbox-example-next.git",
depth: 1, // Shallow clone (optional)
revision: "main", // Branch, tag, or commit (optional)
},
runtime: "node24",
ports: [3000],
});
const sandbox = await Sandbox.create({
source: {
type: "git",
url: "https://github.com/org/private-repo.git",
username: process.env.GIT_USERNAME!,
password: process.env.GIT_TOKEN!, // Use PAT for password
},
runtime: "node24",
});
const sandbox = await Sandbox.create({
source: {
type: "tarball",
url: "https://example.com/project.tar.gz",
},
runtime: "node24",
ports: [3000],
});
const sandbox = await Sandbox.create({
source: {
type: "snapshot",
snapshotId: "snap_abc123",
},
ports: [3000],
});
Instead of a stock runtime, start a sandbox from a Vercel Container
Registry (VCR) image stored in the
sandbox's project. image and runtime are mutually exclusive — pass one
or the other, never both.
const sandbox = await Sandbox.create({
image: "my-repo:v1",
ports: [3000],
});
image accepts a repository in the sandbox's project with an optional tag or
digest, or a fully-qualified VCR URL. A bare repository name resolves to the
latest tag:
await Sandbox.create({ image: "my-repo" }); // latest tag
await Sandbox.create({ image: "my-repo:v1" }); // specific tag
await Sandbox.create({ image: "my-repo@sha256:..." }); // specific digest
await Sandbox.create({
image: "vcr.vercel.com/my-team/my-project/my-repo:v1", // fully-qualified
});
Push images to VCR with Docker-compatible tooling before referencing them. See the Container Registry docs for push instructions.
Sandbox.fork seeds a new sandbox from another sandbox's current snapshot
and copies its config (resources, timeout, networkPolicy, tags,
ports, persistent, snapshotExpiration, keepLastSnapshots). Any field
you pass overrides the inherited value. env is not copied (encrypted
server-side) and must be re-supplied. If the source has no current snapshot,
the fork falls back to a fresh create using the source's runtime plus the
copied config.
// Inherit everything from the source
const fork = await Sandbox.fork({ sourceSandbox: "prod-agent" });
// Override specific fields; the rest are copied from the source
const fork = await Sandbox.fork({
sourceSandbox: "prod-agent",
name: "forked-prod-agent",
resources: { vcpus: 4 },
env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY! },
});
Use await using for automatic cleanup:
async function runInSandbox() {
await using sandbox = await Sandbox.create();
// Sandbox automatically stopped when scope exits
await sandbox.runCommand("echo", ["Hello"]);
}
const result = await sandbox.runCommand("npm", ["install"]);
if (result.exitCode !== 0) {
console.error("Install failed:", await result.stderr());
}
const result = await sandbox.runCommand({
cmd: "npm",
args: ["run", "build"],
cwd: "/vercel/sandbox/app",
env: { NODE_ENV: "production" },
sudo: false,
stdout: process.stdout, // Stream output
stderr: process.stderr,
});
// Start dev server in background
const devServer = await sandbox.runCommand({
cmd: "npm",
args: ["run", "dev"],
detached: true, // Returns immediately
stdout: process.stdout,
});
// Later: wait for completion or kill
const finished = await devServer.wait();
// Supported signals: SIGHUP, SIGINT, SIGQUIT, SIGKILL, SIGTERM, SIGCONT, SIGSTOP (or numeric)
await devServer.kill("SIGTERM");
await sandbox.runCommand({
cmd: "dnf",
args: ["install", "-y", "golang"],
sudo: true, // Execute as root
});
await sandbox.writeFiles([
{
path: "/vercel/sandbox/config.json",
content: Buffer.from(JSON.stringify({ key: "value" })),
},
{
path: "/vercel/sandbox/script.sh",
content: Buffer.from("#!/bin/bash\necho 'Hello'"),
mode: 0o755,
},
]);
// Returns a Buffer object
const buffer = await sandbox.readFileToBuffer({
path: "/vercel/sandbox/output.txt",
});
// Returns a NodeJS.ReadableStream
const stream = await sandbox.readFile({
path: "/vercel/sandbox/large-file.bin",
});
const localPath = await sandbox.downloadFile(
{ path: "/vercel/sandbox/report.pdf" }, // source path on the sandbox
{ path: "./downloads/report.pdf" }, // destination path on the local machine
{ mkdirRecursive: true },
);
await sandbox.mkDir("/vercel/sandbox/my-app/src");
sandbox.fs — node:fs/promises-compatible APIconst content = await sandbox.fs.readFile("/etc/hostname", "utf8");
await sandbox.fs.writeFile("/tmp/hello.txt", "Hello, world!");
const files = await sandbox.fs.readdir("/tmp");
const stats = await sandbox.fs.stat("/tmp/hello.txt");
const sandbox = await Sandbox.create({
networkPolicy: "allow-all",
});
const sandbox = await Sandbox.create({
networkPolicy: "deny-all",
});
const sandbox = await Sandbox.create({
networkPolicy: {
allow: ["*.npmjs.org", "github.com", "registry.yarnpkg.com"],
subnets: {
allow: ["10.0.0.0/8"],
deny: ["10.1.0.0/16"], // Takes precedence over allowed
},
},
});
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"ai-gateway.vercel.sh": [
{
transform: [
{
headers: { authorization: "Bearer ..." },
},
],
},
],
"*": [], // Allow all other domains without transforms
},
},
});
Rules can match on method, path, query string, and headers. All specified dimensions must match; multiple methods are ORed; multiple header and query-string matchers are ANDed.
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"ai-gateway.vercel.sh": [
{
match: {
method: ["POST"],
path: { startsWith: "/v1/" },
headers: [
{ key: { exact: "x-api-key" }, value: { exact: "placeholder" } },
],
},
transform: [{ headers: { authorization: "Bearer ..." } }],
},
],
},
},
});
Matchers support exact, startsWith, and regex (RE2).
Use forwardURL to redirect any matching request through an HTTPS proxy you
control. The proxy receives the original request along with sandbox metadata in
forwarded headers.
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"api.example.com": [
{
match: { path: { startsWith: "/secure/" } },
forwardURL: "https://my-proxy.example.com",
},
],
},
},
});
Implement the proxy handler with defineSandboxProxy, using the Web Request & Response objects — it verifies the
sandbox OIDC token and extracts metadata about the source sandbox:
// app/api/sandbox-proxy/route.ts
import { defineSandboxProxy } from "@vercel/sandbox/proxy";
const handler = defineSandboxProxy(async (request, meta) => {
// meta: { host, teamId, projectId, sandboxId, sandboxName }
console.log("Proxied from sandbox", meta.sandboxName);
return fetch(request);
});
// Sandboxes forward requests using their original method, so the handler
// must be exposed under every verb the network policy can route.
export {
handler as GET,
handler as POST,
handler as PUT,
handler as PATCH,
handler as DELETE,
};
Use sandbox.update (preferred). updateNetworkPolicy is deprecated but still
works.
await sandbox.update({ networkPolicy: { allow: ["api.openai.com"] } });
sandbox.update replaces individual update helpers and accepts any of the
mutable parameters. When ports is provided, it is treated as the full
desired port list — any currently exposed port not present in the array is
deregistered.
await sandbox.update({
resources: { vcpus: 4 }, // Memory auto-scales to 2048 MB per vCPU
timeout: ms("30m"),
networkPolicy: "deny-all",
ports: [3000, 8000],
tags: { env: "prod" },
persistent: false,
snapshotExpiration: ms("14d"),
keepLastSnapshots: { count: 1 },
currentSnapshotId: "snap_xyz", // Rollback to a previous snapshot
});
// Permanently remove a sandbox and all its snapshots.
await sandbox.delete();
stop() is synchronous: it blocks until the VM is fully stopped and returns
the final session state, including the snapshot created during shutdown (when
persistent: true).
const result = await sandbox.stop();
console.log(result.snapshot?.id);
console.log(result.activeCpuUsageMs);
console.log(result.networkTransfer); // { ingress, egress }
Sandboxes support up to 5 key-value tags. Tags can be set at creation, updated
via sandbox.update({ tags }), and used as filters when listing.
await Sandbox.create({ tags: { env: "staging", team: "infra" } });
const result = await Sandbox.list({ tags: { env: "staging" } });
All list APIs use cursor-based pagination and return an async-iterable that auto-paginates through every page. You can also iterate page-by-page or collect all items at once.
Sandbox.listconst result = await Sandbox.list({
namePrefix: "ci-", // Filter by name prefix
tags: { env: "staging" }, // Filter by tags
sortBy: "createdAt", // "createdAt" (default), "name", or "statusUpdatedAt"
sortOrder: "desc", // "asc" or "desc" (default)
limit: 50,
});
// Per-item async iteration (auto-paginates)
for await (const sandbox of result) {
console.log(sandbox.name);
}
// Per-page iteration
for await (const page of result.pages()) {
console.log(page.sandboxes.length);
}
// Collect everything
const all = await result.toArray();
// Or use the cursor directly
const next = result.pagination.next;
sandbox.listSessions and sandbox.listSnapshots// List all VM sessions for this sandbox
const sessions = await sandbox.listSessions();
for await (const session of sessions) {
console.log(session.sessionId, session.status);
}
// List snapshots belonging to this sandbox
const snapshots = await sandbox.listSnapshots();
for await (const snapshot of snapshots) {
console.log(snapshot.snapshotId, snapshot.status);
}
Snapshot.listconst snapshots = await Snapshot.list({
name: "my-dev-env", // Filter by sandbox name
sortOrder: "desc",
limit: 50,
});
for await (const snapshot of snapshots) {
console.log(snapshot.snapshotId, snapshot.status);
}
A Session is a single running VM instance inside a sandbox. You typically do not interact with sessions directly — the SDK creates and resumes them for you — but you can inspect the current one.
const session = sandbox.currentSession();
console.log(session.sessionId);
console.log(session.status); // "pending" | "running" | "stopping" | "stopped" | ...
Snapshots save the entire sandbox filesystem to be reused later, for any number of sandboxes.
const sandbox = await Sandbox.create({ runtime: "node24" });
await sandbox.runCommand("npm", ["install"]);
// Create snapshot (stops the sandbox)
const snapshot = await sandbox.snapshot({
expiration: ms("14d"), // Default: 30 days, use 0 for no expiration
});
console.log("Snapshot ID:", snapshot.snapshotId);
Configure default expiration and retention policy per sandbox:
await Sandbox.create({
name: "my-app",
snapshotExpiration: ms("7d"), // Default TTL for any snapshot of this sandbox
keepLastSnapshots: {
count: 1, // Keep only the most recent snapshot (1-10)
expiration: ms("30d"), // Override expiration for kept snapshots
deleteEvicted: true, // Delete evicted snapshots immediately (default)
},
});
keepLastSnapshots: { count: 1 } is the recommended setting when you only
care about the latest snapshot — it lets the SDK keep snapshot storage costs flat.
// List all snapshots in the project (auto-paginates)
const snapshots = await Snapshot.list();
for await (const snapshot of snapshots) {
console.log(snapshot.snapshotId, snapshot.status);
}
// Get a specific snapshot
const snapshot = await Snapshot.get({ snapshotId: "snap_abc123" });
// Delete a snapshot
await snapshot.delete();
Snapshots form a tree: any sandbox created from another snapshot inherits a parent → child relationship. Walk that tree to see ancestors or descendants of a given snapshot.
// Walk ancestors (default direction)
const ancestors = await Snapshot.tree({
snapshotId: "snap_abc",
sortOrder: "desc",
});
for await (const node of ancestors) {
console.log(node.snapshotId, node.parentId);
}
// Walk descendants
const descendants = await Snapshot.tree({
snapshotId: "snap_abc",
sortOrder: "asc",
});
Point an existing sandbox at a previous snapshot by updating
currentSnapshotId. New sessions will resume from that snapshot.
await sandbox.update({ currentSnapshotId: "snap_previous" });
const sandbox = await Sandbox.create({ ports: [3000, 8000] });
// Get public URL for a port
const url = sandbox.domain(3000);
// Returns: https://subdomain.vercel.run
Replace the exposed port list at runtime with sandbox.update({ ports }). Any
currently exposed port not present in the new array is deregistered.
await sandbox.update({ ports: [3000, 8443] });
const sandbox = await Sandbox.create({
timeout: ms("10m"), // Initial timeout, default of 5 minutes
});
// Extend timeout by 5 more minutes
await sandbox.extendTimeout(ms("5m"));
// New total: 15 minutes
# Pull development credentials
vercel link
vercel env pull
The SDK automatically uses VERCEL_OIDC_TOKEN from environment.
const sandbox = await Sandbox.create({
teamId: process.env.VERCEL_TEAM_ID!,
projectId: process.env.VERCEL_PROJECT_ID!,
token: process.env.VERCEL_TOKEN!,
// ... other options
});
import { APIError, StreamError } from "@vercel/sandbox";
try {
const sandbox = await Sandbox.create();
} catch (error) {
if (error instanceof APIError) {
console.error("API Error:", error.message, error.response.status);
} else if (error instanceof StreamError) {
console.error("Stream Error:", error.message);
}
throw error;
}
const controller = new AbortController();
// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30000);
const sandbox = await Sandbox.create({
signal: controller.signal,
});
const result = await sandbox.runCommand({
cmd: "npm",
args: ["test"],
signal: controller.signal,
});
| Limitation | Details |
| --------------- | ------------------------------------------------------------------------------- |
| Max vCPUs | 4 vCPUs on Hobby, 8 vCPUs on Pro, 32 vCPUs on Enterprise (2048 MB RAM per vCPU) |
| Max ports | 15 exposed ports |
| Max tags | 5 key-value tags per sandbox |
| Max timeout | 24 hours (Pro/Enterprise), 45 minutes (Hobby) |
| Default timeout | 5 minutes |
| Base system | Amazon Linux 2023 |
| User context | vercel-sandbox user |
| Writable path | /vercel/sandbox |
Pre-installed: git, tar, gzip, unzip, curl, openssl, procps, findutils, which.
Install additional packages with sudo:
await sandbox.runCommand({
cmd: "dnf",
args: ["install", "-y", "package-name"],
sudo: true,
});
# Install CLI
pnpm i -g sandbox
# Login / Logout
sandbox login
sandbox logout
# Create and connect
sandbox create --connect
sandbox create --name my-app
sandbox create --image my-repo:v1 # Boot from a VCR image (not with --runtime)
sandbox create --non-persistent # Disable filesystem persistence
sandbox create --snapshot-expiration 7d # Default snapshot TTL
sandbox create --keep-last-snapshots 1 # Retention policy
sandbox create --tag env=staging # Repeatable
# Fork an existing sandbox (inherits config; env is NOT copied)
sandbox fork <source>
sandbox fork <source> --name my-fork --vcpus 4 --env FOO=1
# List sandboxes (paginated, filterable)
sandbox ls
sandbox ls --name-prefix ci- --sort-by name
sandbox ls --tag env=staging --limit 100 --cursor <token>
# Run a command in a new sandbox (create + exec in one step)
sandbox run -- node -e "console.log('hello')"
sandbox run --name my-app -- npm test # Resumes existing sandbox if present
sandbox run --stop -- npm build # Stop the session when the command exits
sandbox run --rm -- npm build # DELETES the sandbox after running
# Execute command in an existing sandbox
sandbox exec <name> -- npm install
sandbox exec <name> --stop -- npm build
# Start an interactive shell
sandbox connect <name>
# Copy files
sandbox cp local-file.txt <name>:/vercel/sandbox/
# Stop sandbox (synchronous; reports snapshot + usage)
sandbox stop <name>
# Permanently delete sandbox and all its snapshots
sandbox remove <name>
# Sessions
sandbox sessions list <name>
# Snapshots
sandbox snapshot <name>
sandbox snapshots list --name <name>
sandbox snapshots get <snapshot-id>
sandbox snapshots remove <snapshot-id>
sandbox snapshots tree <name> # Walk the tree from the sandbox's current snapshot
sandbox snapshots tree <name> --cursor <snapshot-id> --sort-order asc
# Config (view + update any sandbox parameter)
sandbox config list <name>
sandbox config vcpus <name> <count>
sandbox config timeout <name> <duration>
sandbox config persistent <name> <true|false>
sandbox config snapshot-expiration <name> <duration|none>
sandbox config keep-last-snapshots <name> <count>
sandbox config keep-last-snapshots-for <name> <duration|none>
sandbox config delete-evicted-snapshots <name> <true|false>
sandbox config current-snapshot <name> <snapshot-id>
sandbox config network-policy <name> --network-policy deny-all
sandbox config tags <name> --tag env=prod
const sandbox = await Sandbox.getOrCreate({
name: "my-dev-env",
source: { type: "git", url: "https://github.com/org/repo.git" },
ports: [3000],
timeout: ms("30m"),
onCreate: async (sbx) => {
await sbx.runCommand("npm", ["install"]);
},
onResume: async (sbx) => {
await sbx.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true });
},
});
console.log("App running at:", sandbox.domain(3000));
await using sandbox = await Sandbox.create({
source: { type: "git", url: repoUrl },
persistent: false, // Skip snapshotting on shutdown
snapshotExpiration: ms("1d"), // Short TTL for any incidental snapshot
});
const install = await sandbox.runCommand("npm", ["ci"]);
if (install.exitCode !== 0) throw new Error("Install failed");
const build = await sandbox.runCommand("npm", ["run", "build"]);
if (build.exitCode !== 0) throw new Error("Build failed");
const test = await sandbox.runCommand("npm", ["test"]);
process.exit(test.exitCode);
Maintain a single "base" sandbox with dependencies installed, and spawn fresh
children from it with Sandbox.fork. Each fork inherits the base's config
and is seeded from its current snapshot — no need to store snapshot IDs in
your code. New base snapshots are picked up automatically on the next fork.
// Once: bootstrap the base sandbox
await Sandbox.getOrCreate({
name: "my-base",
runtime: "node24",
keepLastSnapshots: { count: 5 }, // Keep storage flat
onCreate: async (sbx) => {
await sbx.runCommand("npm", ["install", "-g", "typescript", "tsx"]);
},
});
// On every run: fork the base sandbox
async function runFromBase(code: string) {
await using sandbox = await Sandbox.fork({
sourceSandbox: "my-base",
persistent: false,
});
await sandbox.writeFiles([
{ path: "/vercel/sandbox/index.ts", content: Buffer.from(code) },
]);
return sandbox.runCommand("tsx", ["index.ts"]);
}
// Idempotent: first call creates, subsequent calls resume
const sandbox = await Sandbox.getOrCreate({
name: `workspace-${userId}`,
runtime: "node24",
keepLastSnapshots: { count: 1, expiration: ms("5d") },
onCreate: async (sbx) => {
await sbx.runCommand("git", ["clone", repoUrl, "."]);
await sbx.runCommand("npm", ["install"]);
},
});
// Use the sandbox — auto-resumes if it was stopped
await sandbox.runCommand("git", ["pull"]);
tools
Creates durable, resumable workflows using Vercel's Workflow SDK. Use when building workflows that need to survive restarts, pause for external events, retry on failure, or coordinate multi-step operations over time. Triggers on mentions of "workflow", "durable functions", "resumable", "workflow sdk", "queue", "event", "push", "subscribe", or step-based orchestration.
tools
Install and configure Vercel Workflow SDK before it exists in node_modules. Use when the user asks to "install workflow", "set up workflow", "add durable workflows", "configure workflow sdk", or "init workflow" for Next.js, Express, Hono, Fastify, NestJS, Nitro, Nuxt, Astro, SvelteKit, or Vite.
tools
Migrates Temporal, Inngest, Trigger.dev, and AWS Step Functions workflows to the Workflow SDK. Use when porting Activities, Workers, Signals, step.run(), step.waitForEvent(), Trigger.dev tasks / wait.forToken / triggerAndWait, ASL JSON state machines, Task/Choice/Wait/Parallel states, task tokens, or child workflows.
tools
Use when building UIs leveraging the WordPress Design System (WPDS) and its components, tokens, patterns, etc.