A harness is the agent inside the sandbox. Pick Grok Build, Claude Code, Codex, OpenCode, or another CLI.
A harness adapter starts that agent inside the sandbox and turns its work into chat() stream chunks. The provider decides where the agent runs. The harness decides which agent runs. You can change one without changing the other.
| Harness | Package | Adapter | Auth |
|---|---|---|---|
| Grok Build | @tanstack/ai-grok-build | grokBuildText | Default 'api-key' (XAI_API_KEY). 'host' uses grok login. |
| Claude Code | @tanstack/ai-claude-code | claudeCodeText | Default 'api-key' (ANTHROPIC_API_KEY). 'host' uses claude login. |
| Codex | @tanstack/ai-codex | codexText | Default 'api-key' (CODEX_API_KEY). 'host' uses codex login. |
| OpenCode | @tanstack/ai-opencode | opencodeText | OPENAI_API_KEY from the process env. No authMode flag. |
| ACP-Compatible | @tanstack/ai-acp | acpCompatible / acpCompatibleText | Default 'api-key' uses authMethodId. 'host' skips ACP authenticate. |
npm i @tanstack/ai-claude-codepnpm add @tanstack/ai-claude-codeyarn add @tanstack/ai-claude-codebun add @tanstack/ai-claude-codeimport { chat } from '@tanstack/ai'
import { claudeCodeText } from '@tanstack/ai-claude-code'
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
const sandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: [
'npm install -g @anthropic-ai/claude-code --include=optional && claude --version',
],
secrets: createSecrets({
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '',
}),
}),
})
const stream = chat({
adapter: claudeCodeText('claude-opus-4-8'),
messages: [{ role: 'user', content: 'Summarize this repo.' }],
threadId: 'thread-1',
middleware: [withSandbox(sandbox)],
})The agent CLI must exist inside the sandbox. The setup step installs it. See Installing an agent CLI.
To change the agent, change the adapter, the secret, and the install step. The provider and the workspace source stay the same.
If you want a typed object after the agent reads the repo, pass outputSchema on the same chat() call. The dedicated adapters and acpCompatible support it. See Harness Agents.
Many coding agents speak the Agent Client Protocol (ACP), for example pi and gemini --acp. See the full list.
If an agent has no dedicated package, acpCompatible from @tanstack/ai-acp builds a harness adapter for it. It is the harness version of openaiCompatible. Tell it how to start the agent, then use it with any provider:
import { acpCompatible } from '@tanstack/ai-acp'
const pi = acpCompatible({
name: 'pi',
models: ['pi-fast', 'pi-pro'],
command: ({ model, harnessCwd }) => `pi --acp -m ${model} --cwd ${harnessCwd}`,
authMethodId: 'pi-api-key',
})The ACP-Compatible Harness guide has the full config: typed models, modelOptions for each call, WebSocket transports, permissions, and protocol coverage. To find agents, see the ACP agents list and the ACP registry.
grokBuildText, claudeCodeText, and codexText can write the agent's stdout to an append-only NDJSON file inside the sandbox (/tmp/tanstack-runs/<runId>.ndjson). The host then reads that file and does not hold the stdout pipe. As a result:
They do this only for a durable run. To opt in, pass withSandbox both runs and durability. A plain withSandbox(sandbox) writes no journal and streams over a pipe.
You must also pass the runId, because the journal path comes from it. A durable run with no runId from the caller throws DurableRunIdRequiredError.
import {
chat,
chatParamsFromRequest,
memoryStream,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { codexText } from '@tanstack/ai-codex'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { withSandbox } from '@tanstack/ai-sandbox'
import { sandbox } from './sandbox'
// Single-process stand-ins. ./takeover has the multi-replica setup.
const persistence = memoryPersistence()
const { runs } = persistence.stores
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request)
const adapter = memoryStream(request)
const stream = chat({
adapter: codexText('gpt-5.3-codex'),
messages,
threadId,
runId, // a later reader finds the journal with this id
// Pass BOTH, or there is no journal.
middleware: [withSandbox(sandbox, { runs, durability: { adapter } })],
})
return toServerSentEventsResponse(stream, { durability: { adapter } })
}The id must be unique for each run. The journal appends, so if you use an id again, a reader stops at the exit line of the earlier run. For what you get without the opt-in, and for replay against a log that was already delivered, see The Run Journal.
opencodeText and acpCompatible harnesses do not read NDJSON from stdout. They have no journal, also when a run is durable.
For the rest of the durable setup, see Durable Runs and Takeover & Detached Runs. Takeover also lists the three exports that a harness adapter implements to support attach.