Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Provisioning (Advanced)
Provisioning writes tokens, skills, MCP servers, and house rules into the files each CLI expects.
Provisioning lets you declare all of it once, on defineWorkspace(). At bootstrap, each harness adapter writes it in the format its CLI reads.
Provision a workspace#
- Wrap your secrets with createSecrets. You get opaque refs, not strings.
- List your skills, plugins, and instructions on defineWorkspace.
- Pass the workspace to defineSandbox, and use the sandbox as usual.
import {
bearer,
createSecrets,
defineSandbox,
defineWorkspace,
fileSkill,
gitSkill,
githubRepo,
mcpSkill,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
const secrets = createSecrets({
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '',
GH: process.env.GH_TOKEN ?? '',
SENTRY: process.env.SENTRY_TOKEN ?? '',
})
export const repoSandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/repo', ref: 'main' }),
// This example uses Claude Code. Install the CLI for your harness.
setup: [
'npm install -g @anthropic-ai/claude-code --include=optional && claude --version',
],
secrets,
skills: [
// A public skill repo.
gitSkill({ repo: 'owner/tanstack-skills' }),
// A private skill repo. The token is used only for the clone.
gitSkill({ repo: 'owner/private-skills', secret: secrets.GH }),
// A third-party MCP server, with a bearer token header.
mcpSkill('sentry', {
url: 'https://mcp.example.com',
headers: { Authorization: bearer(secrets.SENTRY) },
}),
// Any file you want in the workspace.
fileSkill({ path: '.agent-hints.md', content: '# Hints\nPrefer pnpm.' }),
],
plugins: ['@anthropic/plugin-foo'],
instructions: 'Always run `pnpm test` before proposing a change.',
}),
})Start a run with this sandbox and claudeCodeText. The agent can use the skills and the MCP server, and it reads your instructions from AGENTS.md.
How it works#
- Secrets stay as refs everywhere in your config. The real values are resolved only into the live sandbox env, at create, resume, and snapshot restore. They are never written to snapshots, the sandbox store, or the event log.
- Skills, MCP servers, and plugins are written by the harness adapter at bootstrap, each in its CLI's native place. See Per-harness projection.
- Instructions go to AGENTS.md at the workspace root. CLAUDE.md and GEMINI.md point to it.
- A CLI that lacks a concept warns and skips that entry. It does not throw. So one definition works with every harness, and each agent takes the parts it understands.
To limit what the agent is allowed to do (which commands run, which ask, which are denied), use Policy, not instructions.
Advanced#
Why secret values never leak#
createSecrets keeps the real strings in a non-enumerable, symbol-keyed registry on the object it returns. Each property you read (secrets.GH) is a SecretRef token, not the string. So:
- Object.keys(secrets), spreads, and JSON.stringify(secrets) never show the values.
- The workspace definition is safe to hash, store, and replay for resume, with no credential in it.
secrets is declared on the workspace. The harness adapters write secret-bearing MCP config again on every run, with fresh values. So a snapshot never serves a stale or rotated secret.
Where a SecretRef goes#
Pass a ref straight to any field that takes one. For gitSkill, pass secret. The token is resolved only when the repo is cloned.
In MCP headers, use a ref as is, or wrap it with bearer(ref) to get Bearer <value>:
import { bearer, createSecrets, mcpSkill } from '@tanstack/ai-sandbox'
const secrets = createSecrets({
GH: process.env.GH_TOKEN ?? '',
SENTRY: process.env.SENTRY_TOKEN ?? '',
})
export const sentry = mcpSkill('sentry', {
url: 'https://mcp.example.com',
headers: {
Authorization: bearer(secrets.SENTRY), // "Bearer <value>"
'X-Token': secrets.GH, // the raw token value
},
})Skill builders#
| Builder | What it provisions |
|---|---|
| gitSkill | A skill repo cloned into the workspace, with optional secret and clone path. |
| mcpSkill | A third-party MCP server, with url and headers. |
| fileSkill | Any file written into the workspace. |
| agentSkill | A public skill by bare name. Every bundled harness warns and skips it, so use gitSkill instead. |
These MCP servers are third-party services. To give the agent your own app's tools (a chat() server tool whose execute() runs on your host), use Tools.
gitSkill clone path#
into sets where the repo is cloned. It is an absolute path inside the sandbox. The default is .tanstack-skills/<repo-basename> under the workspace root. Bootstrap creates the parent directory before the clone.
A path that starts with /workspace uses the portable workspace root, and the provider maps it to its real workdir. Keep /workspace/... unless you must pin a provider-specific path.
import {
createSecrets,
defineWorkspace,
gitSkill,
githubRepo,
} from '@tanstack/ai-sandbox'
const secrets = createSecrets({ GH: process.env.GH_TOKEN ?? '' })
export const workspace = defineWorkspace({
source: githubRepo({ repo: 'owner/repo' }),
secrets,
skills: [
gitSkill({
repo: 'owner/private-skills',
secret: secrets.GH,
into: '/workspace/.skills/private',
}),
],
})Per-harness projection#
| Harness | MCP servers | gitSkill repos | plugins |
|---|---|---|---|
| Claude Code | .mcp.json | .claude/skills/<name> | claude plugin install <name> --scope project |
| Codex | .codex/config.toml | .codex/skills/<name> | Warns and skips. |
| Grok Build | .grok/config.toml | .grok/skills/<name> | Warns and skips. |
| OpenCode | opencode.json | Warns and skips (cloned, not linked). | Warns and skips. |
For a linked skill repo, the projector looks for folders that hold a SKILL.md:
- A nested pack (skills/foo/SKILL.md) is linked under the name foo.
- A flat clone with SKILL.md at its root uses the clone name.
Links are symlinks. If ln -s fails, the projector copies the folder. You never write the links yourself.
AGENTS.md and its links#
Bootstrap writes instructions to AGENTS.md at the workspace root. It then creates CLAUDE.md and GEMINI.md as symlinks to it. If the sandbox cannot make symlinks, they are copies. Either way, every supported CLI reads the instructions with no extra config.