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:
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 it, so 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.