# 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()`](./workspace), 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](./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:

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

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

| 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](./tools).

## `AGENTS.md` and per-harness symlinks

`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](./policy) instead.
