Skip to content
Sandboxes

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

  1. Write the rules.
  2. Pass them as policy to defineSandbox.
ts
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:

DecisionWhat happens
allowThe agent runs it.
askThe agent pauses. The harness sends an approval request, and the client answers it before the action continues.
denyThe 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:

ts
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:

  1. If a matching rule says deny, the action is blocked.
  2. Else, if a matching rule says ask, the action needs approval.
  3. Else, if a rule says allow, the action runs.
  4. If nothing matches, the default applies.

So you can allow a broad family and block one case:

ts
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.networkWhat sbxSandbox() writes
No policy, and no allowNetwork or denyNetworkIf 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 allowNetworkAllow the model API host (for a known adapter), localhost, and allowNetwork for this sandbox. Then apply denyNetwork.
No policy, and denyNetwork onlyDeny those hosts for this sandbox. Allow stays empty: no automatic hosts and no localhost. This adds deny rules on top of the machine preset.
allowAllow **, then apply denyNetwork.
denyAllow 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.

ts
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.