Skip to content
Adapters

Grok Build

The Grok Build adapter runs xAI's Grok Build coding agent as a chat backend. Unlike HTTP provider adapters, this is a harness adapter: Grok Build runs its own agent loop and executes its own tools — shell commands, file edits, search — by spawning the grok CLI inside a sandbox. 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.

Requires a sandbox. grok-build declares requires: [SandboxCapability], so chat() errors at the call site unless you provide a sandbox with withSandbox(...) middleware. The sandbox — your laptop, a Docker container, or a cloud VM — is the filesystem and safety boundary the agent runs in. See the Sandboxes overview for the full picture.

Installation

shell
npm i @tanstack/ai-grok-build @tanstack/ai-sandbox

You also need a sandbox provider (e.g. @tanstack/ai-sandbox-docker) and the grok CLI available inside the sandbox image.

Authentication

Your laptop can already have grok login. A CI runner only has XAI_API_KEY. Both can use the same sandbox provider. The default authMode is 'api-key'. Set 'host' when you want grok login. See Harness Auth.

ts
import { grokBuildText } from "@tanstack/ai-grok-build"

grokBuildText("composer-2.5")
grokBuildText("composer-2.5", { authMode: "host" })
  • 'api-key' (default): inject XAI_API_KEY and authenticate with it.
  • 'host': skip ACP authenticate. Use grok login. Do not inject XAI_API_KEY into that process.

The two modes list the model under slightly different ids. The adapter maps the short alias for you (see Models).

Basic Usage

ts
import { chat } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import {
  createSecrets,
  defineSandbox,
  defineWorkspace,
  githubRepo,
  withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
import { messages, threadId } from './chat-context'

const sandbox = defineSandbox({
  id: 'grok-build-agent',
  provider: dockerSandbox({ image: 'node:22' }),
  workspace: defineWorkspace({
    source: githubRepo({ repo: 'owner/app' }),
    setup: ['corepack enable', 'pnpm install'],
    secrets: createSecrets({ XAI_API_KEY: process.env.XAI_API_KEY ?? '' }),
  }),
})

const stream = chat({
  threadId,
  adapter: grokBuildText('composer-2.5'),
  messages,
  middleware: [withSandbox(sandbox)],
})

Models

Grok Build accepts any xAI model id its backend supports; the known ids get autocomplete (any string is allowed):

Model idNotes
grok-buildThe short alias. With a grok.com browser login the CLI lists it under this name.
grok-build-0.1The fully-qualified id the CLI lists when authenticated with XAI_API_KEY.
composer-2.5Also runnable through the Grok Build harness.

Pass any of these to grokBuildText(...) — the adapter resolves grok-build to the CLI's grok-build-0.1 automatically, so the same code works under both auth modes.

Configuration

Adapter config (second argument to grokBuildText):

OptionDescription
cwdWorking directory inside the sandbox. Defaults to /workspace.
grokExecutablePath/name of the grok executable inside the sandbox. Defaults to grok.
envExtra environment variables for the grok process inside the sandbox.
emitDiffEmit a file.changed CUSTOM event with the working-tree git diff after the run. Defaults to true.
protocolHarness wire protocol: 'acp' or 'streaming-json'. Defaults to 'acp'. A durable sandbox run with no protocol set uses 'streaming-json' so the run can journal.
authMode'api-key' (default) uses XAI_API_KEY. 'host' uses grok login. See Harness Auth.
authMethodIdExplicit ACP auth method. Wins over authMode.
extraArgsExtra raw CLI flags appended verbatim (advanced).

Per-call overrides go through modelOptions:

modelOptionsDescription
sessionIdResume an existing Grok Build session (see below).
cwdPer-call override of the harness working directory.
maxTurnsPer-call cap on the number of harness turns.
protocolPer-call override of the harness wire protocol.
authModePer-call 'host' or 'api-key'. Default 'api-key'.
authMethodIdPer-call ACP auth method. Wins over authMode.

Stateful Sessions

Grok Build sessions are stateful — the harness keeps the working context (files read, commands run, conclusions reached) between turns. The adapter surfaces the session id of every fresh run as a custom stream event named grok-build.session-id; thread it back via modelOptions.sessionId to resume. When resuming, only the latest user message is sent — the harness already holds the prior context.

ts
import { chat, chatParamsFromRequest, toServerSentEventsResponse } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import { withSandbox } from '@tanstack/ai-sandbox'
import { sandbox } from './sandbox'

export async function POST(request: Request) {
  const params = await chatParamsFromRequest(request)
  const sessionId =
    typeof params.forwardedProps.sessionId === 'string'
      ? params.forwardedProps.sessionId
      : undefined

  const stream = chat({
    adapter: grokBuildText('grok-build'),
    messages: params.messages,
    middleware: [withSandbox(sandbox)],
    modelOptions: { sessionId },
  })

  return toServerSentEventsResponse(stream)
}

Tools

Two kinds of tools flow through this adapter:

  1. Built-in harness tools are executed by Grok Build itself (shell, file edits, search) and stream back as tool-call events with results already attached. Your code never executes them.
  2. Your TanStack tools are bridged into the harness over an authenticated MCP tool-proxy: define them with toolDefinition().server() and pass them to chat({ tools }). Tool-call events come back under the names you registered. Because the harness runs in a sandbox, see Sandbox tools for how the bridge reaches your host across providers (local/Docker vs cloud).

Client-side and approval-gated tools are not supported — the harness runs tools inside a live process and can't pause across an HTTP round-trip. A tool without a server execute() (or marked needsApproval) fails fast; run those with a regular provider adapter.

Tool choice

You want Grok Build to see just one of your tools, or none of them. Pass toolChoice to chat(). It limits only the tools that the adapter bridges into Grok Build. The built-in Grok Build tools always stay on, and Grok Build decides when it calls a tool. The rules are the same on both protocols.

ValueWhat the adapter does
'auto'Bridges all of your tools.
'none'Bridges none of your tools.
{ type: 'tool', name }Bridges only the named tool. Grok Build does not have to call it.
'required'Bridges all of your tools. Grok Build does not have to call one.

Each value except 'auto' logs a warning the first time an adapter instance gets it.

For the values on other adapters, see Choose when the model calls a tool.

Durable runs

A durable sandbox run (you pass runs and durability to withSandbox) journals the agent output so a later request can take the run over. ACP does not journal. When durability is wired and you do not set protocol, the adapter uses 'streaming-json'.

Set protocol: 'acp' only when you want ACP on that call. A fresh ACP run with durability wired logs a warning. An attach against ACP throws DurableAttachNotSupportedError.

Headless NDJSON flags

On the 'streaming-json' path with auto-approve, the adapter adds --always-approve --no-plan --no-auto-update. Those flags keep Plan Mode and the CLI update check from blocking a headless run.

Structured Output

Pass outputSchema on chat(). Grok Build has no native schema flag. The adapter adds the JSON Schema to the prompt (ACP and streaming-json) and parses the last assistant text. Tool activity still streams. The object arrives as structured-output.complete.

ts
import { chat } from "@tanstack/ai"
import { grokBuildText } from "@tanstack/ai-grok-build"
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: grokBuildText("grok-build"),
  messages: [{ role: "user", content: "Review this repo." }],
  outputSchema: Report,
  middleware: [withSandbox(sandbox)],
})

report.summary

This path parses JSON from the last assistant message. If extract-only is the job, use @tanstack/ai-grok.

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

Full walkthrough, including the client: Harness Agents.

Limitations

  • Requires a sandbox. Always run it under withSandbox(...); see the Sandboxes overview.
  • Server-only (Node). The harness spawns the grok CLI in a sandbox.
  • The harness owns the agent loop. TanStack's agent-loop strategies and per-iteration middleware don't apply inside a harness turn.
  • No sampling controls. temperature-style options don't exist here.
  • Cold starts. Each call runs a full harness turn; expect higher first-token latency than HTTP adapters.

This adapter ignores wrapFetch, because the harness process sends the model requests.