@tanstack/ai-sandbox-upstash-box, point it at your API key, and a coding agent driven by chat() runs inside a Box with a real filesystem, shell, background processes, preview URLs, and snapshots. No glue code.
A sandboxed run has three independent parts:
- Provider: where the agent runs. That is Upstash Box here.
- Workspace: what the agent sees. The repo, the package manager, setup commands, secrets.
- Harness: which agent runs. Claude Code, Codex, Grok Build, OpenCode, or any ACP agent.
1. Install
@tanstack/ai: thechat()pipeline.@tanstack/ai-sandbox:defineSandbox,defineWorkspace,withSandbox.@tanstack/ai-sandbox-upstash-box: the Upstash Box provider.@tanstack/ai-claude-code: the harness adapter. Swap it for@tanstack/ai-codexor@tanstack/ai-grok-buildif you prefer another agent.
2. Set your environment variables
Get a Box API key from the Upstash Console..env
upstashBoxSandbox() reads UPSTASH_BOX_API_KEY when you omit apiKey. The harness key is different: it belongs to the agent running inside the box, so it travels as a workspace secret.
3. Define the sandbox
defineSandbox() binds a provider, a workspace, and a lifecycle into one reusable object.
sandbox.ts
PATH. The node runtime already ships claude, codex, and opencode in /usr/local/bin, so those three need no setup step at all.
For any other CLI, install it with sudo -n. A box runs as the non-root boxuser, and /usr/local/lib/node_modules is root-owned, so a plain npm install -g fails with EACCES and takes the whole bootstrap down with it:
npm install -g can exit 0 with a broken install. Running the CLI in the same step turns that into a loud bootstrap failure instead of a broken run.
Secrets are injected into the box environment on create and resume. They are never written to snapshots, the sandbox store, or the event log.
TanStack AI’s conventional
/workspace virtual root maps to the box home directory, /workspace/home. That is the handle’s workspaceRoot, and it is where the cloned repo lands.4. Run the agent
withSandbox() is chat() middleware. It resumes or creates the box, bootstraps the workspace, runs the harness inside it, and tears everything down according to the lifecycle.
run.ts
threadId keys the box. Two runs on the same thread reuse the same box instead of cloning and installing again.
Harness runs emit standard AG-UI chunks (text, tool calls, reasoning) plus a namespaced CUSTOM event. When the run finishes, the adapter emits file.changed with the working tree git diff, which is the change the agent made.
To serve the same run from a server route, return the stream as Server-Sent Events:
app/api/chat/route.ts
5. Preview the app the agent builds
If the agent starts a dev server inside the box,ports.connect() mints a public preview URL for it. It is a method on the SandboxHandle, which you get from the provider directly (step 7) or from the sandbox instance inside a middleware hook:
publicUrlAuth on the provider to gate them:
{ bearerToken: true } returns a token plus an Authorization: Bearer header. { basicAuth: true } returns Basic credentials instead.
6. Skip the cold start with snapshots
Cloning the repo and installing dependencies is the expensive part of a run. Upstash Box supports native snapshots, sosnapshot: "after-setup" is the default: bootstrap runs once, the result is snapshotted, and later runs resume from it.
There are two
keepAlive settings and they are unrelated. The one in lifecycle is TanStack’s duration hint ("30m") for how long a sandbox should stay warm between runs. The one on upstashBoxSandbox({ keepAlive }) is Box’s boolean: false (the default) lets the box auto-pause when idle, true keeps it running and billing.restoreSnapshot() rebuilds a fresh box from it with Box.fromSnapshot(), and resume by id probes the box status first, so a deleted box resumes as null rather than a dead handle.
fork() works the same way, snapshot plus create from that snapshot. It costs a full snapshot round trip, roughly 25 seconds.
7. Use the box without a harness
The provider also gives you the uniformSandboxHandle directly, with no chat() or agent involved. This is the quickest way to check that your key and runtime are wired correctly.
smoke-test.ts
Provider options
What the provider supports
Things to know
Spawned processes are tied to the handle.spawn() runs the command as a live exec.session over a WebSocket, which is what gives it a real pid, a writable stdin, and server-side signals. The session owns its process, so dropping the connection kills the command and sessions cannot be reattached. A spawned process lives as long as the handle, not as long as the box.
network: "deny" is strict. It maps to Box’s deny-all egress mode, which blocks every outbound connection. Providers that model deny as an allowlist are more permissive. An agent that works under one of those will not reach package registries or model provider hosts here. Leave the capability unset if the agent needs either, and use network policies on the box for domain and CIDR rules.
Bridged tools need a tunnel in local development. Tools you pass to chat() execute on your host and are bridged into the box over an authenticated proxy the box dials back to. A box is a remote machine, so it cannot reach localhost on your laptop. Deployed orchestrators work as they are. For local development, tunnel the bridge with withNgrokBridge. See the TanStack AI tools guide.
Next steps
- Sandbox providers for the full provider reference.
- Workspace for repo sources, setup groups, and scripts.
- Harnesses for running Codex, Grok Build, or any ACP agent instead.
- Snapshots and preview URLs for what the box does underneath.