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
Policy
A policy allows a command, asks for approval, or blocks it. Use it to stop sudo, a repo delete, or a call to an unknown host.
A policy holds those rules. defineSandboxPolicy() writes them once, and each harness adapter maps them to its own permission system.
Add a policy#
- Write the rules.
- Pass them as policy to defineSandbox.
import {
defineSandbox,
defineSandboxPolicy,
defineWorkspace,
githubRepo,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
const policy = defineSandboxPolicy({
commands: {
allow: ['pnpm test', 'pnpm typecheck', 'git diff'],
ask: ['pnpm install', 'curl *'],
deny: ['sudo *', 'rm -rf *'],
},
capabilities: { fileWrite: 'allow', network: 'ask' },
default: 'ask',
})
export const sandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: ['corepack enable', 'pnpm install'],
}),
policy,
})Every run that uses this sandbox now follows the policy, including the bridged tools. The policy does not check the workspace setup commands. They come from your own config and run as written.
Decisions#
Each command or capability gets one of three decisions:
| Decision | What happens |
|---|---|
| allow | The agent runs it. |
| ask | The agent pauses. The harness sends an approval request, and the client answers it before the action continues. |
| deny | The action is blocked. The agent cannot run it. |
Commands#
commands has three lists of patterns: allow, ask, and deny. A pattern matches the command that the agent is about to run.
- * is a glob. curl * matches every curl command. sudo * matches everything run through sudo.
- A string with no *, such as pnpm test, matches only that command.
Put your named workspace scripts (pnpm test, pnpm build) in allow. They give the policy stable names to match.
Capabilities#
capabilities uses the same decisions for broad abilities:
import { defineSandboxPolicy } from '@tanstack/ai-sandbox'
const policy = defineSandboxPolicy({
capabilities: {
fileWrite: 'allow', // the agent can edit the working tree
network: 'ask', // pause for approval before any outbound request
},
})This is the backstop. A network command that is not in your commands lists still needs approval under network: 'ask'.
If you omit a capability, it uses the policy default. Some providers also enforce network: 'deny' when they create the sandbox. See Providers (Advanced).
Precedence: deny > ask > allow#
When more than one rule matches, the strictest one wins:
- If a matching rule says deny, the action is blocked.
- Else, if a matching rule says ask, the action needs approval.
- Else, if a rule says allow, the action runs.
- If nothing matches, the default applies.
So you can allow a broad family and block one case:
import { defineSandboxPolicy } from '@tanstack/ai-sandbox'
const policy = defineSandboxPolicy({
commands: {
// Every `curl` is allowed...
allow: ['curl *'],
// ...but `deny` wins, so this host is always blocked.
deny: ['curl * internal.example.com*'],
},
default: 'deny',
})The default#
default is the decision for anything that no rule matches. If you omit it, it is 'ask'.
- 'allow': only what you ask about or deny is gated. Good for trusted dev loops.
- 'ask': unknown actions pause for approval. A safe middle.
- 'deny': the agent runs only what you allow. The strictest choice, for untrusted or production runs.
How ask reaches the user#
When the agent tries an ask action, the harness pauses it and sends an approval request into the run stream. Your client approves or rejects it. Until the client answers, the action waits.
Use ask for actions that are usually fine but sometimes dangerous, such as pnpm install with a new dependency, or an outbound curl. The human stays in the loop, and you do not have to list every safe command.
Advanced#
How adapters map a policy#
Each harness adapter translates the same policy into its own permissions:
- Grok Build: the permission flags of the grok CLI.
- Claude Code: its permission rules (allowed, ask, and denied tool and command rules).
- Codex: its approval and sandbox settings.
- Other harnesses: whatever native gate they have.
If a harness cannot express a rule, it skips that rule with a warning. The run does not fail.
A deny-only list with default: 'allow' stays permissive on Grok Build and Codex. Those harnesses keep auto-approve and do not enforce commands.deny. The outer sandbox is the isolation there. If you need a deny rule for each command, use Claude Code.
Some providers run as a non-root user, so package installs in setup need sudo. Do not deny sudo * on those providers. See Providers (Advanced) for the list, and Workspace for how to write setup commands.
Network policy on Docker Sandboxes#
Most providers leave capabilities.network to the harness. sbxSandbox() also enforces it on the host HTTP and HTTPS proxy (networkPolicy: true).
sbx policy can allow or deny hosts. It has no ask. Deny wins over allow, so network: 'deny' becomes an allowlist, not a full deny.
| TanStack capabilities.network | What sbxSandbox() writes |
|---|---|
| No policy, and no allowNetwork or denyNetwork | If the policy list is empty, sbxSandbox() runs sbx policy init deny-all. A known adapter (grok-build, claude-code, codex) then allows its model API host and localhost for this sandbox. Unknown adapters stay on the machine preset. |
| No policy, and allowNetwork | Allow the model API host (for a known adapter), localhost, and allowNetwork for this sandbox. Then apply denyNetwork. |
| No policy, and denyNetwork only | Deny those hosts for this sandbox. Allow stays empty: no automatic hosts and no localhost. This adds deny rules on top of the machine preset. |
| allow | Allow **, then apply denyNetwork. |
| deny | Allow the model API host, localhost, and allowNetwork. Then apply denyNetwork. |
| ask (or the policy default when network is unset) | The same allowlist as deny. The harness still asks for tools and commands. |
Model hosts that are allowed automatically:
- grok-build: api.x.ai
- claude-code: api.anthropic.com
- codex: api.openai.com
- opencode or an unknown adapter: none
If the allowlist is empty under deny or ask, create throws. Pass allowNetwork, or use grokBuildText, claudeCodeText, or codexText.
The guest calls host.docker.internal for the tool bridge. The sbx proxy changes that host to localhost before the policy match. sbxSandbox() adds localhost when it writes a real allowlist. It does not add it for denyNetwork only.
import { sbxSandbox } from '@tanstack/ai-sandbox-docker'
import { defineSandbox, defineSandboxPolicy } from '@tanstack/ai-sandbox'
const sandbox = defineSandbox({
id: 'repo-agent',
provider: sbxSandbox({
allowNetwork: ['*.npmjs.org', 'registry.npmjs.org'],
}),
policy: defineSandboxPolicy({
capabilities: { network: 'deny' },
commands: { allow: ['pnpm test'], deny: ['sudo *'] },
}),
})Command rules and fileWrite stay with the harness, because sbx does not see command lines.