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.
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.',
})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.
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.
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.
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:
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 })],
})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:
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 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:
| Builder | What it provisions |
|---|---|
| agentSkill | A named public skill (portable placeholder; Claude Code warns and skips — use gitSkill instead). |
| gitSkill | A skill repo cloned into the workspace, with optional auth and clone path. |
| mcpSkill | A third-party MCP server, with URL and headers. |
| fileSkill | An arbitrary file written into the workspace. |
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 takes an optional into field — an absolute path inside the sandbox — controlling where the repo is cloned. It defaults to .tanstack-skills/<repo-basename>:
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',
}),
],
})At bootstrap each harness projector maps these values into its CLI's native format:
| Harness | MCP servers projected to |
|---|---|
| Claude Code | .mcp.json |
| Codex | .codex/config.toml |
| OpenCode | opencode.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.
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.