TanStack
Adapters

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

shell
npm i @tanstack/ai-claude-code

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

ts
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

ts
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

OptionDescription
cwdWorking directory for the harness session. Defaults to process.cwd().
permissionModeClaude Code permission mode ('default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk', 'auto'). See the permissions note below.
allowedToolsBuilt-in tools the harness may use without prompting (e.g. ['Read', 'Grep', 'Bash(npm test:*)']).
disallowedToolsBuilt-in tools removed from the harness entirely.
maxTurnsMaximum 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.
mcpServersExtra 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.
apiKeyAnthropic API key for the harness subprocess.
envExtra environment variables for the harness subprocess.
pathToClaudeCodeExecutableUse a specific Claude Code executable instead of the SDK's bundled one.
streamPartialsEmit true token-level deltas for text and tool-call arguments (default true). When false, both emit whole from the complete message.
canUseToolCustom permission handler; replaces the adapter's default handler.
settingSourcesClaude 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.

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:

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

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

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

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

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

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.

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

On the client, pass the same schema to useChat and read final. partial stays empty until the end.

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