- OpenAIUpdated
- Anthropic
- Google Gemini
- Google Vertex AI
- Ollama
- Grok (xAI)
- GroqUpdated
- Mistral
- Cohere
- TypeSafe
- Ollaya
- ElevenLabs
- fal.ai
- Reactor
- World Labs
- OpenRouter AdapterUpdated
- Perplexity Search
- Vercel AI Gateway
- Lovable AI Gateway
- OpenAI-CompatibleUpdated
- LLM Gateway
- CloudflareUpdated
- Claude Code
- Codex
- OpenCode
- Grok Build
- ACP-Compatible
- Amazon BedrockUpdated
- BytePlus
Claude Code
The Claude Code adapter runs Claude Code (via the @anthropic-ai/claude-agent-sdk) as a chat backend. Unlike HTTP provider adapters, this is a harness adapter: Claude Code runs its own agent loop and executes its own tools — bash, file reads and edits, glob/grep search, web search — locally on your server. Each chat() call runs one full harness turn; the harness's tool activity streams back as already-resolved tool-call events your UI can render.
Server-only. The harness spawns the Claude Code runtime as a subprocess, so this adapter only works in a Node.js server environment — never in the browser. Treat it like giving Claude a shell on the machine it runs on, and configure permissions accordingly.
Installation#
npm i @tanstack/ai-claude-codepnpm add @tanstack/ai-claude-codeyarn add @tanstack/ai-claude-codebun add @tanstack/ai-claude-codeA runnable demo lives at examples/sandbox-cloudflare — pick Claude Code, Codex, or Grok Build in the UI, with session resume, the harness tool timeline, and tool bridging, wired into a TanStack Start app on Workers. For the same wiring on plain Node with durable, refresh-surviving runs (this adapter on Docker), see examples/sandbox-web.
Authentication#
Your laptop can already have claude login. A CI runner only has ANTHROPIC_API_KEY. The default authMode is 'api-key'. Set 'host' when you want claude login. See Harness Auth.
import { claudeCodeText } from "@tanstack/ai-claude-code"
claudeCodeText("claude-opus-4-8")
claudeCodeText("claude-opus-4-8", { authMode: "host" })- 'api-key' (default): inject ANTHROPIC_API_KEY (or pass apiKey).
- 'host': use claude login. Do not inject ANTHROPIC_API_KEY.
Basic Usage#
import { chat } from "@tanstack/ai";
import { claudeCodeText } from "@tanstack/ai-claude-code";
const stream = chat({
adapter: claudeCodeText("claude-opus-4-8", {
cwd: "/path/to/project",
permissionMode: "acceptEdits",
}),
messages: [{ role: "user", content: "Fix the failing test in utils.test.ts" }],
});RUN_FINISHED.usage.promptTokens counts the full input, cached tokens included. See Token usage.
Configuration#
| Option | Description |
|---|---|
| cwd | Working directory for the harness session. Defaults to process.cwd(). |
| permissionMode | Claude Code permission mode ('default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk', 'auto'). See the permissions note below. |
| allowedTools | Built-in tools the harness may use without prompting (e.g. ['Read', 'Grep', 'Bash(npm test:*)']). |
| disallowedTools | Built-in tools removed from the harness entirely. |
| maxTurns | Maximum harness-internal turns per run. |
| systemPromptMode | 'append' (default) keeps Claude Code's preset system prompt and appends your systemPrompts; 'replace' sends yours as the entire prompt. |
| mcpServers | Extra MCP servers passed through to the harness untouched. |
| authMode | 'api-key' (default) injects ANTHROPIC_API_KEY. 'host' uses claude login. Also valid on modelOptions. See Harness Auth. |
| apiKey | Anthropic API key for the harness subprocess. |
| env | Extra environment variables for the harness subprocess. |
| pathToClaudeCodeExecutable | Use a specific Claude Code executable instead of the SDK's bundled one. |
| streamPartials | Emit true token-level deltas for text and tool-call arguments (default true). When false, both emit whole from the complete message. |
| canUseTool | Custom permission handler; replaces the adapter's default handler. |
| settingSources | Claude Code settings tiers to load. Default ['project']: the workspace's CLAUDE.md, .claude/skills, and .mcp.json apply, and the host's ~/.claude (plugins, hooks, skills) is not loaded. Pass ['user', 'project', 'local'] for CLI-equivalent behavior, or [] to load no settings. |
Permissions on headless servers. Without an explicit permissionMode or canUseTool, the adapter installs a safe default handler: bridged TanStack tools always run, and any built-in tool call that would normally prompt a human is denied with guidance instead of hanging the request. To let the harness edit files or run commands, set permissionMode: 'acceptEdits' / 'bypassPermissions', or enumerate allowedTools.
Reasoning#
Set reasoning on chat(). The adapter turns it into a Claude Code setting:
import { chat } from "@tanstack/ai";
import { claudeCodeText } from "@tanstack/ai-claude-code";
const stream = chat({
adapter: claudeCodeText("claude-opus-4-8"),
messages: [{ role: "user", content: "Refactor the auth module" }],
reasoning: "xhigh",
});- claude-opus-4-8, claude-opus-4-7, opus: --effort, low to max.
- claude-opus-4-6, claude-sonnet-4-6, sonnet: --effort, low, medium, high, max.
- claude-haiku-4-5, haiku: MAX_THINKING_TOKENS, off to high. The level picks the budget, or you set it with budgetTokens. off sends 0.
Stateful Sessions#
Claude Code sessions are stateful — the harness keeps the full working context (files read, commands run, conclusions reached) between turns. The adapter surfaces the session id of every run as a custom stream event named claude-code.session-id; thread it back via modelOptions.sessionId to resume the session. When resuming, only the latest user message is sent — the harness already holds the prior context.
The claude-code.session-id event payload also includes skills, an array of skills loaded during SDK initialization (or an empty array when none are reported).
Server endpoint:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { claudeCodeText } from "@tanstack/ai-claude-code";
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);
// Extra fields the client puts in the connection `body` arrive here.
const sessionId =
typeof params.forwardedProps.sessionId === "string"
? params.forwardedProps.sessionId
: undefined;
const stream = chat({
adapter: claudeCodeText("claude-opus-4-8", {
cwd: "/path/to/project",
permissionMode: "acceptEdits",
}),
messages: params.messages,
modelOptions: { sessionId },
});
return toServerSentEventsResponse(stream);
}Client (React) — capture the session id from the custom event and send it back on subsequent requests:
import { useState } from "react";
import { useChat } from "@tanstack/ai-react";
import { fetchServerSentEvents } from "@tanstack/ai-client";
function CodingAssistant() {
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat", () => ({
body: { sessionId },
})),
onCustomEvent: (name, value) => {
if (
name === "claude-code.session-id" &&
typeof value === "object" &&
value !== null &&
"sessionId" in value &&
typeof value.sessionId === "string"
) {
setSessionId(value.sessionId);
}
},
});
// ... render messages; harness tool activity (Bash, Edit, Read, ...)
// arrives as regular tool-call parts with their results attached.
}Sessions are stored on the machine that ran them (~/.claude/projects/), so resuming only works on the same server instance. Pass modelOptions: { forkSession: true } alongside sessionId to branch a session instead of continuing it.
Tools#
Two kinds of tools flow through this adapter:
Built-in harness tools (Bash, Read, Write, Edit, Glob, Grep, WebSearch, ...) are executed by Claude Code itself. Their activity streams back as tool-call events with results already attached, so useChat UIs render them with no extra wiring — but your code never executes them.
Your TanStack tools are bridged into the harness as an in-process MCP server. Define them as usual with toolDefinition().server(); the model sees them as mcp__tanstack__<name> and the adapter strips the prefix on the way back out, so events match the names you registered.
import { z } from "zod";
import { chat, toolDefinition } from "@tanstack/ai";
import { claudeCodeText } from "@tanstack/ai-claude-code";
const lookupTicket = toolDefinition({
name: "lookup_ticket",
description: "Look up an issue ticket by id",
inputSchema: z.object({ ticketId: z.string() }),
}).server(async ({ ticketId }) => {
return { ticketId, status: "open", title: "Crash on startup" };
});
const stream = chat({
adapter: claudeCodeText("claude-opus-4-8"),
messages: [{ role: "user", content: "What's the status of ticket T-123?" }],
tools: [lookupTicket],
});Client-side and approval-gated tools are not supported. The harness executes tools inside a live subprocess, which cannot pause across HTTP requests to wait for a browser round-trip or a human approval. Passing a tool without a server execute() implementation — or one marked needsApproval — fails fast with a descriptive error. Run those tools outside the harness with a regular provider adapter.
Tool choice#
You want Claude Code to answer in text only, or to use just one of your tools. Pass toolChoice to chat():
const onlyLookup = chat({
adapter: claudeCodeText("claude-opus-4-8"),
messages: [{ role: "user", content: "What's the status of ticket T-123?" }],
tools: [lookupTicket],
toolChoice: { type: "tool", name: "lookup_ticket" },
});| Value | What the adapter does |
|---|---|
| 'auto' | No change. Claude Code can call every tool. |
| 'none' | Turns off the built-in tools and bridges none of your tools. Claude Code answers in text. |
| { type: 'tool', name } | Turns off the built-in tools and bridges only the named tool. Claude Code can call only this tool, but it does not have to. |
| 'required' | No change. Claude Code cannot force a tool call, so the adapter logs a warning the first time. |
For 'none' and a named tool, the adapter passes --tools "" and --strict-mcp-config. Then other MCP servers, for example from the workspace or a settings file, do not load.
For the values on other adapters, see Choose when the model calls a tool.
Structured Output#
Pass outputSchema on chat(). Claude Code runs one harness turn, uses its native tools, and returns a typed object. The schema JSON is passed to --json-schema as inline JSON (the CLI rejects a file path). Tool activity and prose stream as usual. The object arrives as structured-output.complete, including when Claude delivers it through its built-in StructuredOutput tool.
By default, the adapter loads only project settings (--setting-sources project). Workspace projections (instructions, skills, MCP config) are project-scoped, so they apply. The host's ~/.claude is not loaded, so a local-process run does not pick up your personal skills and hooks. A cloned repo's .claude/settings.json and hooks apply to the run. Set settingSources to change this. The adapter does not pass --bare, because that flag ignores a host claude login.
On local-process, Claude uses your host claude login. On Docker, pass ANTHROPIC_API_KEY in the process env. A container has no host login.
import { chat } from "@tanstack/ai"
import { claudeCodeText } from "@tanstack/ai-claude-code"
import { defineSandbox, withSandbox } from "@tanstack/ai-sandbox"
import { dockerSandbox } from "@tanstack/ai-sandbox-docker"
import { z } from "zod"
const Report = z.object({
summary: z.string(),
filesChanged: z.array(z.string()),
})
const sandbox = defineSandbox({
id: "repo-report",
provider: dockerSandbox({ image: "node:22" }),
})
const report = await chat({
adapter: claudeCodeText("claude-opus-4-8"),
messages: [{ role: "user", content: "Review this repo." }],
outputSchema: Report,
middleware: [withSandbox(sandbox)],
})
report.summaryOn the client, pass the same schema to useChat and read final. partial stays empty until the end.
import { fetchServerSentEvents, useChat } from "@tanstack/ai-react"
import { z } from "zod"
const Report = z.object({
summary: z.string(),
filesChanged: z.array(z.string()),
})
function ReportView() {
const { final, isLoading } = useChat({
connection: fetchServerSentEvents("/api/repo-report"),
outputSchema: Report,
})
if (isLoading) return <p>The agent is inspecting the repo.</p>
if (!final) return null
return <p>{final.summary}</p>
}If you only need to extract JSON from a prompt and do not need a sandbox, use @tanstack/ai-anthropic. That path is faster.
Full walkthrough, including the client: Harness Agents.
Limitations#
- Server-only (Node). The harness spawns a subprocess; Windows support is untested.
- The harness owns the agent loop. TanStack's agent-loop strategies and per-iteration middleware don't apply inside a harness turn; maxTurns is the equivalent control.
- No sampling controls. temperature-style options don't exist here.
- Sessions are machine-local. Resume requires hitting the same server instance.
- Cold starts. Each call spawns a harness turn; expect higher first-token latency than HTTP adapters.
This adapter ignores wrapFetch, because the harness process sends the model requests.