Sandbox SDK

One TypeScript API for agent sandboxes. Files, commands, ports, snapshots, and a typed escape hatch, across every major provider.

8
providers
gated
capabilities
MIT
license
Local
Blaxel
Cloudflare
CodeSandbox
Daytona
E2B
Modal
Vercel
import { withSandbox } from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";await withSandbox(  {    adapter: local(),    cwd: "/workspace",  },  async (sandbox) => {    await sandbox.files.write("main.ts", "console.log('hello')");    const result = await sandbox.process.shell("bun main.ts");    console.log(result.stdout);  });

01Why

Every sandbox provider exposes a slightly different API for the same handful of primitives. @sandbox-sdk/core exposes the slice that's the same everywhere (files, processes, ports, snapshots) behind one small contract, and gets out of the way for anything provider-specific.

  • One small API across providers. Swap E2B for Daytona without rewriting your agent loop.
  • Web-standards I/O. Accepts string, Uint8Array, Blob, ArrayBuffer, or a ReadableStream. Runs on Node, Bun, Workers. Anywhere the fetch primitives run.
  • Capabilities, not surprises. Each adapter declares what it supports. Branch on supports(sandbox, "snapshotCreate") and supports(sandbox, "snapshotDelete") and supports(sandbox, "snapshotRestore") instead of discovering it the hard way in production.
  • Escape hatch via sandbox.raw. The native client is always one property away, typed per adapter, for anything outside the unified surface.
  • Predictable errors. A single SandboxError with a normalized code across providers, and the original error attached as cause.

02Installation

Install @sandbox-sdk/core together with the adapter you want to run against. Each provider ships as its own package, so apps only install what they use.

bun add @sandbox-sdk/core @sandbox-sdk/local

03Quick start

Call withSandbox with an adapter to get a typed Sandbox that is stopped automatically after success or failure. The adapter is fixed at construction; there is no runtime { provider, ... } form, which keeps call sites flat and lets the raw property stay narrowly typed.

import { withSandbox } from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";await withSandbox(  {    adapter: local(),    cwd: "/workspace",  },  async (sandbox) => {    await sandbox.files.write("main.ts", "console.log('hello')");    const result = await sandbox.process.shell("bun main.ts");    console.log(result.stdout);  });

04Adapters

Each adapter ships as its own package. Bring only what you use; the others stay out of your node_modules. Pass credentials and provider settings explicitly when you want deterministic behavior; otherwise the wrapped provider SDK can use its own defaults.

The list below covers what ships today. More providers are planned; the capability matrix at the bottom of the page is the authoritative source for what's supported right now.

Local

Local filesystem and child process. The dev/test adapter: point it at a directory and it implements the same Sandbox contract as the cloud adapters using node:fs/promises and node:child_process. It is not an isolation boundary for untrusted code. Every sandbox path is mapped below the root, and existing symlinks are rejected when they resolve outside it. Ports return localhost URLs, and filesystem snapshots support create, delete, and restore in the same process.

import { create } from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";const sandbox = await create({  adapter: local({    root: "./.sandbox",    keep: true,  }),  cwd: "/workspace",});

Options

Blaxel

Blaxel perpetual sandboxes via @blaxel/core. The adapter maps Blaxel files, process execution, background processes, and preview URLs onto the shared Sandbox SDK surface.

Pass apiKey and workspace explicitly for deterministic auth, or let the Blaxel SDK use BL_API_KEY, BL_CLIENT_CREDENTIALS, BL_WORKSPACE, or the Blaxel CLI config.

Treat native client session tokens as bearer credentials and create them on a trusted backend. Agent Drive is a private-preview workspace-wide feature, so drivePath and readOnly mounts are not authorization boundaries for untrusted sandbox code. Restrict access to the drive identity token and blfs when that boundary matters.

import { create } from "@sandbox-sdk/core";import { blaxel } from "@sandbox-sdk/blaxel";const sandbox = await create({  adapter: blaxel({    image: "blaxel/base-image:latest",  }),  cwd: "/app",});

Options

Cloudflare

Cloudflare via @cloudflare/sandbox. Backed by a Durable Object running a Linux container, so the adapter takes a binding from env rather than an API key, and all I/O stays on Cloudflare's network.

The Worker must export Sandbox from @cloudflare/sandbox and bind that Durable Object in wrangler.jsonc. The adapter uses Cloudflare's RPC transport and exposes ports through zero-config HTTPS tunnels. Set tunnel when one port needs a stable named tunnel, or use tunnels to map multiple ports to distinct labels in your Cloudflare zone. For Worker-managed WebSocket upgrades, use the typed native sandbox.raw.wsConnect(request, port) escape hatch.

import type { Sandbox as CloudflareSandbox } from "@cloudflare/sandbox";import { create } from "@sandbox-sdk/core";import { cloudflare } from "@sandbox-sdk/cloudflare";export { Sandbox } from "@cloudflare/sandbox";type Env = {  Sandbox: DurableObjectNamespace<CloudflareSandbox>;};export default {  async fetch(request: Request, env: Env) {    const sandbox = await create({      adapter: cloudflare({ binding: env.Sandbox }),      cwd: "/workspace",    });    await sandbox.files.write("/workspace/main.ts", "console.log('hello')");    if (request.headers.get("upgrade")?.toLowerCase() === "websocket") {      return sandbox.raw.wsConnect(request, 8080);    }    const result = await sandbox.process.shell("bun /workspace/main.ts");    return new Response(result.stdout);  },};

Set backups to opt into normalized filesystem snapshots through Cloudflare R2 backups. Add a BACKUP_BUCKET binding and, in production, the R2 presigned URL credentials to the Worker. Snapshot creation uses the adapter cwd and names are persisted in Cloudflare backup metadata. Restore writes back to that cwd. Production restores are copy-on-write mounts, so restore again after a sandbox sleeps or restarts. Configure an R2 lifecycle rule because backup TTL limits restoration but does not delete stored objects. Snapshot deletion and fresh sandbox creation from a backup stay unavailable through the normalized API.

const sandbox = await create({  adapter: cloudflare({    binding: env.Sandbox,    backups: {      useGitignore: true,      ttl: 86_400,    },  }),  cwd: "/workspace",});const snapshot = await sandbox.snapshots.create("before-upgrade");await sandbox.snapshots.restore(snapshot.id);

For Node apps and other non-Worker runtimes, deploy Cloudflare's HTTP bridge and use cloudflareBridge(). It keeps normalized files, command execution, and HTTPS tunnel previews available over HTTP.ports.expose() creates a zero-config quick tunnel by default; set tunnel for one named port or tunnels for per-port labels when the bridge Worker has the required Cloudflare account and zone credentials. Bridge working directories stay below /workspace, so relative values resolve there, custom directories are created, and external paths fail before a bridge request. Bridge lifecycle, sessions, persist, hydrate, bucket mounts, warm-pool controls, health, OpenAPI schema access, and raw tunnel controls stay typed on sandbox.raw. PTY support returns a typed WebSocket connection descriptor so your app can own the terminal client and start a long-running service before calling ports.expose(). The bridge HTTP API does not expose a lifecycle-safe background process endpoint, so process.spawn() stays unavailable. SANDBOX_API_KEY authenticates the bridge and is rejected from sandbox environment configuration.

import { create } from "@sandbox-sdk/core";import { cloudflareBridge } from "@sandbox-sdk/cloudflare";const sandbox = await create({  adapter: cloudflareBridge({    url: process.env.SANDBOX_API_URL,    token: process.env.SANDBOX_API_KEY,  }),});await sandbox.files.write("/workspace/main.ts", "console.log('hello')");const result = await sandbox.process.shell("bun /workspace/main.ts");console.log(result.stdout);

Options

CodeSandbox

CodeSandbox microVMs via @codesandbox/sdk. The adapter creates or resumes sandboxes, connects a session, and normalizes files, commands, background commands, and opened ports.

Use template to fork from a template sandbox, or snapshot to fork from a snapshot created with snapshots.create(). Use id on create() to resume an existing sandbox. The hibernated id names the source sandbox, so normalized snapshot deletion stays unsupported.

import { create } from "@sandbox-sdk/core";import { codesandbox } from "@sandbox-sdk/codesandbox";const sandbox = await create({  adapter: codesandbox({    template: "template-sandbox-id",  }),  cwd: "/project/sandbox",});

Options

Daytona

Daytona dev environments via @daytona/sdk. The adapter spins up a workspace from the given image, mounts a workdir, and threads files and processes through Daytona's API.

Use the shared snapshot create option, or the adapter snapshot default, to start from a Daytona snapshot reference. Network limits are configured at creation time in the normalized adapter. Durable Daytona snapshots can be removed through snapshots.delete() when the API key has Daytona's delete:snapshots permission; snapshot creation stays on the native client until the source lifecycle has a matching stable contract. Daytona's native raw.updateNetworkSettings() is still available when the account tier supports runtime network changes.

Standard private previews work through preview.request(), which retains Daytona's preview token. Set signedPreviewonly when an external client needs a self-contained URL. Standard tokens reset after a sandbox restart, so expose the port again after restarting.

import { create } from "@sandbox-sdk/core";import { daytona } from "@sandbox-sdk/daytona";const sandbox = await create({  adapter: daytona({    image: "ubuntu:22.04",  }),});

Options

E2B

E2B's microVM sandboxes via e2b. The adapter can pin a template at construction and threads writes, commands, ports, and snapshots through the E2B SDK.

Use the shared snapshot create option to start a fresh E2B sandbox from a snapshot id. E2B snapshots capture filesystem and memory state, briefly pausing the source sandbox and dropping active command, PTY, and WebSocket connections. Use template for provider template ids and names. Named snapshots return E2B's persisted canonical name, which can include a namespace and tag. Call snapshots.delete() after the snapshot is no longer needed.

When E2B restricts preview traffic, call preview.request() after ports.expose(). It retains the traffic access header without returning it in serializable data.

import { create } from "@sandbox-sdk/core";import { e2b } from "@sandbox-sdk/e2b";const sandbox = await create({  adapter: e2b({    template: "base",  }),});

Options

Modal

Modal sandboxes via modal. The adapter creates sandboxes inside a Modal app, maps file reads and writes through Modal's sandbox filesystem, and exposes provider-declared ports through Modal tunnels. Reconnecting by sandbox id discovers existing tunnels automatically.

Modal supports filesystem snapshot creation and deletion. In-place restore and background process handles stay unsupported in the normalized adapter until the provider exposes a matching stable primitive.

import { create } from "@sandbox-sdk/core";import { modal } from "@sandbox-sdk/modal";const sandbox = await create({  adapter: modal({    image: "alpine:3.21",    ports: [3000],  }),  cwd: "/app",  ports: [3000],});

Options

Vercel

Vercel via @vercel/sandbox. Backed by Vercel's Fluid Compute: named, persistent microVMs with snapshots, dynamic ports, network policy, sessions, interactive PTY connections, and provider-native lifecycle controls. The adapter normalizes files, commands, ports, and snapshots while keeping the full Vercel SDK on raw.

Use snapshot to start from a snapshot id, or snapshots.restore() to point the current named sandbox at a snapshot and resume from it on the next operation. Call snapshots.delete() to permanently remove a durable snapshot after its dependents have been created. Deleting the current snapshot of a stopped named sandbox makes the next getOrCreate() rebuild it.

Command timeouts are enforced inside the sandbox and backed by local cancellation. Use raw.openInteractive() when an application needs Vercel's native controller-backed PTY connection.

import { create } from "@sandbox-sdk/core";import { vercel } from "@sandbox-sdk/vercel";const sandbox = await create({  adapter: vercel({    runtime: "node24",  }),});const preview = await sandbox.ports.expose(3000);

Options

05API reference

Every method lives on the Sandbox instance returned by create(). The unified surface only covers what every adapter can do cleanly; anything provider-specific lives on sandbox.raw.

Top-level exports

These come from @sandbox-sdk/core. create and withSandbox produce the sandbox; the capability helpers let you branch on what a provider supports before calling into it.

import {  capabilityMode,  create,  isSandboxError,  SandboxError,  supports,  withSandbox,} from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";const sandbox = await create({ adapter: local() });if (supports(sandbox, "ports")) {  await sandbox.ports.expose(3000);}
  • create(options): creates or connects to a sandbox and returns a typed Sandbox.
  • withSandbox(options, fn): runs fn with a sandbox and stops it after success or failure.
  • supports(sandbox, capability): returns true when a normalized capability is available.
  • capabilityMode(sandbox, capability): returns the provider-specific mode for a capability, such as "disk" versus "memory" snapshots.
  • supportsRaw(...) / rawCapabilityMode(...): the same checks for powers reached through sandbox.raw.
  • requireCapability(...) / requireRawCapability(...): throw SandboxError with code: "unsupported" when a capability is missing.
  • SandboxError / isSandboxError(error): the normalized error class and a type guard for it.
  • fromSandboxRuntime(runtime): for adapter authors, lifts a low-level SandboxRuntime into the public Sandbox API, using direct bounded commands when available and stream-first process handles otherwise.

files.read(path) / files.text(path)

Reads a file. read returns raw Uint8Array bytes; text decodes as UTF-8. Both throw SandboxError when the path escapes the sandbox root.

// raw bytesconst bytes = await sandbox.files.read("data/in.bin");// → Uint8Array// utf-8 textconst source = await sandbox.files.text("main.ts");

files.stream(path)

Reads a file as ReadableStream<Uint8Array>. Every adapter returns a web stream. For large files, check capabilityMode(sandbox, "fileStreaming"): "native" receives bytes incrementally, while "buffered" exposes a stream after the provider SDK has loaded the file.

const stream = await sandbox.files.stream("data/large.bin");for await (const chunk of stream) {  await destination.write(chunk);}

files.write(path, input)

Writes a file, creating parent directories as needed. Accepts string, Uint8Array, ArrayBuffer, Blob, or ReadableStream<Uint8Array>. Streams are drained before the write completes.

await sandbox.files.write("main.ts", "console.log('hi')");await sandbox.files.write("data/in.bin", new Uint8Array([0x68, 0x69]));await sandbox.files.write("upload.bin", file.stream()); // ReadableStream

files.list(path?)

Lists the immediate children of path (defaults to the sandbox cwd). Returns a sorted, frozen array of Entry values. Each carries path, kind, plus size and modified where the adapter has them cheaply.

const entries = await sandbox.files.list("src");// → readonly Entry[] sorted by path//   each entry: { path, kind: "file" | "directory", size?, modified? }

files.remove(path)

Removes a file or directory recursively when the provider supports it. Missing paths follow provider semantics, so catch SandboxError with code: "not_found" or code: "provider" when deleting optional paths.

await sandbox.files.remove("dist");// removes files and directories recursively when the provider supports it

process.shell(command, options?) / process.exec(command, args?, options?)

Runs a command to completion, buffering stdout and stderr. Use shell for normal shell strings and exec for explicit argv execution.

const result = await sandbox.process.shell("bun main.ts", {  cwd: "src",  env: { NODE_ENV: "production" },  signal: controller.signal,  timeout: 30_000,});// → { code, signal?, stdout, stderr }
const result = await sandbox.process.exec("bun", ["main.ts"], {  cwd: "src",});// → argv execution without shell parsing

Options

process.spawn(command, args?, options?)

Starts command and returns a handle immediately. Use output to stream merged stdout + stderr, optional stdout and stderr when the provider exposes separate streams, kill() to terminate, and result for the final { code, signal?, stdout, stderr }.

const proc = await sandbox.process.spawn("bun", ["watch.ts"]);// stream merged stdout + stderr as bytes flowfor await (const chunk of proc.output) {  process.stdout.write(chunk);}// providers with separate streams expose stdout and stderr tooconst stdout = proc.stdout ? await new Response(proc.stdout).text() : "";const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";// or kill it on a signalawait proc.kill("SIGTERM");// either way, await the final resultconst result = await proc.result;

ports.expose(port, options?)

Returns a provider-aware preview for port. Its request() retains header-based access credentials outside serialized data. Use request() to call a same-origin endpoint. Local sandboxes return derived localhost URLs; provider adapters may return public tunnels, create-time port URLs, or reject unsupported exposure with SandboxError. Branch on sandbox.capabilities.ports first.

Treat a provider-issued signed or tokenized url as a credential. It is intentionally usable by an external client and may therefore contain provider access data.

Requests handle redirects manually by default and reject redirect: "follow", so provider access headers cannot leave the preview origin.

const preview = await sandbox.ports.expose(3000, {  protocol: "https",});const response = await preview.request("/health");console.log(preview.url, preview.port, response.status);

Options

snapshots.create(name?) / snapshots.delete(id) / snapshots.restore(id)

Captures provider state when snapshotCreate is supported. Snapshots can be removed with delete() when snapshotDelete is supported. For persistent provider snapshots, deletion is permanent. Restore means in-place restore of the current sandbox and is tracked separately with snapshotRestore. To create a fresh sandbox from a snapshot, pass snapshot to create() on adapters that advertise snapshotSource. Snapshot names are accepted only when the provider persists them. Other adapters reject a name rather than silently discarding it.

import { supports } from "@sandbox-sdk/core";if (supports(sandbox, "snapshotCreate")) {  const snap = await sandbox.snapshots.create();  if (supports(sandbox, "snapshotRestore")) {    await sandbox.snapshots.restore(snap.id);  }  if (supports(sandbox, "snapshotDelete")) {    await sandbox.snapshots.delete(snap.id);  }}

06The Sandbox type

Sandbox is a frozen record of the five capability namespaces (files, process, ports, snapshots, raw) plus identifiers and a lifecycle hook. capabilities declares what the underlying provider can do through the normalized API. Provider-specific powers live under capabilities.raw and are available through sandbox.raw.

import type {  Capabilities,  Files,  Ports,  Process,  Snapshots,} from "@sandbox-sdk/core";type Sandbox<Raw = unknown> = Readonly<{  id: string;  provider: string;  cwd: string;  capabilities: Capabilities;  files: Files;  process: Process;  ports: Ports;  snapshots: Snapshots;  raw: Raw;  stop(): Promise<void>;}>;

The Raw type parameter is set per-adapter (the local adapter sets it to { root: string }; cloud adapters set it to their native client) so sandbox.raw stays autocomplete-friendly without losing the unified shape. Import the capability types instead of copying their unions: the generated API reference stays the source of truth as the contract evolves.

07Errors

Normalized SDK methods throw SandboxError with a normalized code and the provider name on provider. When a provider failure is wrapped, the original provider error is attached as cause. Native calls through sandbox.raw keep native provider errors.

import { SandboxError } from "@sandbox-sdk/core";try {  await sandbox.ports.expose(3000);} catch (err) {  if (err instanceof SandboxError && err.code === "unsupported") {    return new Response("preview unavailable");  }  throw err;}

Codes

  • "unsupported": the adapter doesn't implement the method. Branch on capabilities to avoid hitting this path.
  • "path_escape": a file or cwd path resolved outside the sandbox root. Always thrown by the local adapter's safety check.
  • "not_found": referenced path or snapshot id does not exist.
  • "timeout": process.shell or process.exec hit options.timeout and the partial output is attached to cause.
  • "aborted": caller cancellation through options.signal.
  • "provider": anything else. Inspect cause for the underlying error.

08Escape hatch

When you need a feature outside the unified surface, like provider-specific networking, GPU attach, custom snapshots, or container internals, drop down to the native client. The raw property is typed per adapter so you keep autocomplete, while the rest of your agent loop stays portable.

// typed per adapter. the local adapter exposes { root }, E2B exposes// the sandbox client, cloudflare exposes the durable object stub, etcconst { root } = sandbox.raw;await inspectWorkspace(root);// cloud example: drop down to E2B's native filesystem watchersandbox.raw.files.watchDir("src", (event) => {  console.log(event);});

09AI tools

@sandbox-sdk/ai wraps a configured sandbox into ready-made tools for agent frameworks. The kit includes prompt context plus file, command, directory, preview tools, and an AI SDK-compatible sandbox object with JSON-schema inputs. Pick the framework that matches your stack; each tool is just a thin shim around files, process, and ports on the underlying sandbox.

Requested operations the adapter cannot perform are omitted from the model-facing tool set. The AI SDK session only starts background commands when the provider exposes separate stdout and stderr streams, so it never presents combined output as standard output.

Installation

bun add @sandbox-sdk/ai

Quick start

Pass a configured sandbox into tools(). The return value is prompt context and a record of tool definitions, each with a description, inputSchema, and execute function. Use aisdk(), openai(), or claude() when you want the exact adapter shape for a framework.

import { create } from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";import { aisdk, tools } from "@sandbox-sdk/ai";const sandbox = await create({ adapter: local() });const kit = tools(sandbox, {  allow: ["read", "write", "list", "exec"],});const ai = aisdk(kit);await kit.tools.write?.execute({  path: "main.ts",  text: "console.log('hi')",});await kit.tools.exec?.execute({ command: "bun main.ts" });

Vercel AI SDK

The root package has no hard dependency on ai. aisdk() returns the shape used by Vercel AI SDK: system, tools, and experimental_sandbox. The same tool definitions work with v6 and v7 style calls, including generateText, streamText, and agent loops that forward the sandbox to tool execution. With AI Gateway, pass a provider/model string directly as model.

Use network(sandbox) when trusted host code also needs lifecycle, ports, or raw provider controls. The returned session is AI SDK-compatible, while restricted() returns a separate session without the host-owned backend.

import { generateText } from "ai";import { create } from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";import { aisdk, tools } from "@sandbox-sdk/ai";const sandbox = await create({ adapter: local() });const kit = aisdk(tools(sandbox, {  allow: ["read", "write", "list", "exec"],}));const result = await generateText({  model: "openai/gpt-5.4-nano",  ...kit,  prompt: "Write a TypeScript program that prints fib(10), then run it.",});

OpenAI Agents SDK

Install @openai/agents and import @sandbox-sdk/ai/openai. The adapter uses the real OpenAI tool() helper, emits JSON-schema parameters, names tools with a sandbox_ prefix by default, and requires approval for side-effect tools unless you opt into autonomous execution.

import { Agent, run } from "@openai/agents";import { create } from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";import { tools } from "@sandbox-sdk/ai";import { openai } from "@sandbox-sdk/ai/openai";const sandbox = await create({ adapter: local() });const kit = openai(tools(sandbox, {  allow: ["read", "write", "list", "exec"],}));const agent = new Agent({  name: "sandbox agent",  instructions: kit.instructions,  tools: Object.values(kit.tools),});const result = await run(agent, "Create a package.json and run bun test.");

Claude Agent SDK

Install @anthropic-ai/claude-agent-sdk and import @sandbox-sdk/ai/claude. Claude custom tools run through an in-process MCP server, so the adapter returns mcpServers, allowedTools, canUseTool, and prompt context for systemPrompt. Read-only tools are annotated as safe, while write, exec, and preview require approval by default.

Use { requireApproval: false } only for disposable sandboxes where the agent should run without a human approval loop.

import { query } from "@anthropic-ai/claude-agent-sdk";import { create } from "@sandbox-sdk/core";import { local } from "@sandbox-sdk/local";import { tools } from "@sandbox-sdk/ai";import { claude } from "@sandbox-sdk/ai/claude";const sandbox = await create({ adapter: local() });const kit = claude(tools(sandbox, {  allow: ["read", "write", "list", "exec"],}));for await (const message of query({  prompt: "Set up a Bun project, install zod, and write a parser.",  options: {    systemPrompt: {      type: "preset",      preset: "claude_code",      append: kit.instructions,    },    mcpServers: kit.mcpServers,    allowedTools: kit.allowedTools,    canUseTool: kit.canUseTool,    tools: [],  },})) {  handle(message);}

Safety model

read and list are safe by default. write, exec, and preview are side-effect tools and must be enabled explicitly. OpenAI and Claude adapters expose approval controls at the framework layer, and every tool still runs through the policy hooks configured in tools().

10Capability matrix

Every adapter implements the same core capability surface, but providers differ on what they natively support. Branch on sandbox.capabilities at runtime, or read this table at design time. Hover the warning and error icons for the why behind each one.

CapabilityLocalBlaxelCloudflareCodeSandboxDaytonaE2BModalVercel
files
file streaming
process exec
process spawn
ports
snapshot create
snapshot delete
snapshot restore
snapshot source
environment
secrets

SupportedSupported with caveatNot supported

11Verification

Every adapter is verified against the live provider, not just mocked. Sanitized fixtures give fast contract replay in bun test, and the verify:* scripts run the same suite against real sandboxes as the source of truth for provider behavior. The deterministic suite never loads .env.local, while live scripts load it explicitly and print readiness without leaking secret values.

# check which provider credentials are presentbun run verify:envbun run test# run the full live suite across every providerbun run verify:providers# verify a single provider end to endbun run verify:vercelbun run verify:cloudflarebun run verify:cloudflare:bridgebun run verify:e2bbun run verify:daytona:snapshot-delete