Sandboxes

Provisioning

Provisioning is how you hand the in-sandbox agent everything beyond the working tree: typed secrets, skill repos, MCP servers, plugins, and a universal instruction file. You declare it all on defineWorkspace(), and each harness adapter projects it into its own native format at bootstrap — so the same definition works whether the agent is Grok Build, Claude Code, Codex, or OpenCode.

ts
import {
  bearer,
  createSecrets,
  defineWorkspace,
  fileSkill,
  gitSkill,
  githubRepo,
  mcpSkill,
} from '@tanstack/ai-sandbox'

const secrets = createSecrets({
  GH: process.env.GH_TOKEN ?? '',
  SENTRY: process.env.SENTRY_TOKEN ?? '',
})

defineWorkspace({
  source: githubRepo({ repo: 'owner/repo', ref: 'main' }),
  secrets,
  skills: [
    gitSkill({ repo: 'owner/tanstack-skills' }),
    gitSkill({ repo: 'owner/private-skills', secret: secrets.GH }),
    mcpSkill('my-mcp', {
      url: 'https://mcp.example.com',
      headers: { Authorization: bearer(secrets.SENTRY) },
    }),
    fileSkill({ path: '.agent-hints.md', content: '# Hints\nPrefer pnpm.' }),
  ],
  plugins: ['@anthropic/plugin-foo'],
  instructions: 'Always run `pnpm test` before proposing a change.',
})

Type-safe secrets

createSecrets turns plain environment values into opaque SecretRef tokens. You pass the refs around your config; the underlying strings stay out of every serializable surface.

ts
import { createSecrets, defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'

const secrets = createSecrets({
  GH: process.env.GH_TOKEN ?? '',
  SENTRY: process.env.SENTRY_TOKEN ?? '',
})

defineWorkspace({
  source: githubRepo({ repo: 'owner/repo', ref: 'main' }),
  secrets,
})

secrets is declared on the workspace, and the values are injected into the sandbox env at create/resume time.

Why the values never leak

The real strings are held in a non-enumerable, symbol-keyed registry on the object createSecrets returns. Each property you access (secrets.GH) is a SecretRef token, not the string. Because the registry is symbol-keyed and non-enumerable:

  • Object.keys(secrets), spreads, and JSON.stringify(secrets) never expose the values.

  • The values are never written to snapshots, the sandbox store, or the event log — only resolved at the moment the sandbox env is built.

    This is what makes the workspace definition safe to hash, persist, and replay for resume bookkeeping without ever persisting a credential.

Passing a secret where a SecretRef is accepted

Hand a ref directly to any field that takes one. The clearest example is gitSkill auth — pass secret: secrets.GH and the token is resolved only when the repo is cloned:

ts
import { createSecrets, defineWorkspace, gitSkill, githubRepo } from '@tanstack/ai-sandbox'

const secrets = createSecrets({ GH: process.env.GH_TOKEN ?? '' })

defineWorkspace({
  source: githubRepo({ repo: 'owner/repo' }),
  secrets,
  skills: [gitSkill({ repo: 'owner/private-skills', secret: secrets.GH })],
})

bearer(ref) for header values

In MCP header values you can use a ref directly, or wrap it with bearer(ref) to produce a Bearer <value> string at resolution time:

ts
import { bearer, createSecrets, mcpSkill } from '@tanstack/ai-sandbox'

const secrets = createSecrets({
  GH: process.env.GH_TOKEN ?? '',
  SENTRY: process.env.SENTRY_TOKEN ?? '',
})

mcpSkill('my-mcp', {
  url: 'https://mcp.example.com',
  headers: {
    Authorization: bearer(secrets.SENTRY), // resolves to "Bearer <value>"
    'X-Token': secrets.GH, // resolves to the raw token value
  },
})

Skills, plugins, and MCP servers

skills is an array of skill values that provision capabilities into the agent's environment. There are four builders, each describing a different kind of capability:

BuilderWhat it provisions
agentSkillA named public skill (portable placeholder; Claude Code warns and skips — use gitSkill instead).
gitSkillA skill repo cloned into the workspace, with optional auth and clone path.
mcpSkillA third-party MCP server, with URL and headers.
fileSkillAn arbitrary file written into the workspace.
ts
import {
  bearer,
  createSecrets,
  defineWorkspace,
  fileSkill,
  gitSkill,
  githubRepo,
  mcpSkill,
} from '@tanstack/ai-sandbox'

const secrets = createSecrets({
  GH: process.env.GH_TOKEN ?? '',
  SENTRY: process.env.SENTRY_TOKEN ?? '',
})

defineWorkspace({
  source: githubRepo({ repo: 'owner/repo' }),
  secrets,
  skills: [
    // Clone a public skill repo (preferred over agentSkill on Claude Code).
    gitSkill({ repo: 'owner/tanstack-skills' }),
    // Clone a private skill repo; `secret` is resolved from the secrets registry.
    gitSkill({ repo: 'owner/private-skills', secret: secrets.GH }),
    // Wire an MCP server with a resolved bearer token in the Authorization header.
    mcpSkill('my-mcp', {
      url: 'https://mcp.example.com',
      headers: { Authorization: bearer(secrets.SENTRY) },
    }),
    // Write an arbitrary file into the workspace.
    fileSkill({ path: '.agent-hints.md', content: '# Hints\nPrefer pnpm.' }),
  ],
  plugins: ['@anthropic/plugin-foo'],
})

gitSkill clone path

gitSkill takes an optional into field — an absolute path inside the sandbox — controlling where the repo is cloned. It defaults to .tanstack-skills/<repo-basename>:

ts
import { createSecrets, defineWorkspace, gitSkill, githubRepo } from '@tanstack/ai-sandbox'

const secrets = createSecrets({ GH: process.env.GH_TOKEN ?? '' })

defineWorkspace({
  source: githubRepo({ repo: 'owner/repo' }),
  secrets,
  skills: [
    gitSkill({
      repo: 'owner/private-skills',
      secret: secrets.GH,
      into: '/workspace/.skills/private',
    }),
  ],
})

Per-harness projection

At bootstrap each harness projector maps these values into its CLI's native format:

HarnessMCP servers projected to
Claude Code.mcp.json
Codex.codex/config.toml
OpenCodeopencode.json

A concept a given CLI lacks — for example, plugins on Codex — emits a warning and is silently skipped rather than throwing. The same applies to agentSkill on Claude Code: there is no reliable primitive to install a public skill by bare name, so the projector warns and skips it. Prefer gitSkill (or a plugins entry) when you need that skill on Claude Code. That keeps one portable definition usable across harnesses: you declare everything once, and each agent takes the parts it understands.

These MCP servers are third-party services you point the agent at. Bridging your own app's host tools into the agent (a chat() server tool whose execute() runs back on the host) is a different mechanism — see Tools.

instructions is a string written to AGENTS.md at the workspace root during bootstrap. Harness-specific counterparts — CLAUDE.md, GEMINI.md — are created as symlinks to it; if the sandbox process layer cannot symlink, they are written as copies instead. Either way the instruction content is read natively by every supported CLI without extra config.

ts
import { defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'

defineWorkspace({
  source: githubRepo({ repo: 'owner/repo' }),
  instructions: 'Always run `pnpm test` before proposing a change.',
})

Use instructions for guidance you want the agent to always follow. To constrain what it is allowed to do — which commands and capabilities are allowed, asked about, or denied — reach for Policy instead.