The Grok Build adapter runs the grok CLI in a sandbox and streams each step to chat(). The agent edits files, runs commands, and searches a repo.
You need:
Do you only want a Grok chat, with no repo and no shell? Use the Grok adapter.
npm i @tanstack/ai-grok-build @tanstack/ai-sandbox @tanstack/ai-sandbox-dockerpnpm add @tanstack/ai-grok-build @tanstack/ai-sandbox @tanstack/ai-sandbox-dockeryarn add @tanstack/ai-grok-build @tanstack/ai-sandbox @tanstack/ai-sandbox-dockerbun add @tanstack/ai-grok-build @tanstack/ai-sandbox @tanstack/ai-sandbox-dockerPut the key in the environment of your server:
XAI_API_KEY=xai-...The sandbox in step 3 gives this key to the grok process as a workspace secret.
Did the machine already run grok login? Then you can use that login and no key. See Harness Auth.
Define the sandbox once. It clones your repo into a Docker container and installs the grok CLI with GROK_CLI_INSTALL_COMMAND:
// sandbox.ts
import { GROK_CLI_INSTALL_COMMAND } from "@tanstack/ai-grok-build";
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
} from "@tanstack/ai-sandbox";
import { dockerSandbox } from "@tanstack/ai-sandbox-docker";
export const sandbox = defineSandbox({
id: "grok-build-agent",
provider: dockerSandbox({ image: "node:22" }),
workspace: defineWorkspace({
source: githubRepo({ repo: "owner/app" }),
setup: [GROK_CLI_INSTALL_COMMAND],
secrets: createSecrets({
XAI_API_KEY: process.env.XAI_API_KEY ?? "",
}),
}),
});Then pass the adapter and the sandbox to chat() in your route:
// api/agent.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 { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: grokBuildText("grok-build"),
messages,
threadId,
runId,
middleware: [withSandbox(sandbox)],
});
return toServerSentEventsResponse(stream);
}Replace owner/app with your repo.
The client is the same as for any chat. Render the text, and show each tool that the agent ran:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function CodingAgent() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/agent"),
threadId: "support-chat",
});
return (
<form
onSubmit={(event) => {
event.preventDefault();
sendMessage(input);
setInput("");
}}
>
{messages.map((message) =>
message.parts.map((part, index) => {
if (part.type === "text") {
return <p key={`${message.id}-${index}`}>{part.content}</p>;
}
if (part.type === "tool-call") {
return <code key={part.id}>{part.name}</code>;
}
return null;
}),
)}
<input value={input} onChange={(event) => setInput(event.target.value)} />
</form>
);
}Ask it "Fix the failing test in utils.test.ts". You see the agent's text, plus a line for each tool it used. The agent edited the files inside the container.
For a version of this setup that runs as a script and prints the diff, see the Sandbox Quick Start.
To learn more about sandboxes, see Sandboxes.
Grok Build keeps its working context between turns: the files it read, the commands it ran, and what it found. The adapter sends the session id of each new run as a custom event named grok-build.session-id. Send it back in modelOptions.sessionId to continue the session.
On the server, read the id from the request:
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);
// 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: grokBuildText("grok-build"),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
middleware: [withSandbox(sandbox)],
modelOptions: { sessionId },
});
return toServerSentEventsResponse(stream);
}On the client, keep the id from the custom event and send it with each request:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function useCodingAgent() {
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
return useChat({
connection: fetchServerSentEvents("/api/agent", () => ({
body: { sessionId },
})),
threadId: "support-chat",
onCustomEvent: (name, value) => {
if (
name === "grok-build.session-id" &&
typeof value === "object" &&
value !== null &&
"sessionId" in value &&
typeof value.sessionId === "string"
) {
setSessionId(value.sessionId);
}
},
});
}On a resumed session, the adapter sends only the latest user message. The agent already has the rest.
The agent has its own tools for files and the shell. It cannot reach your database or your secrets. Pass your server tools to chat(), and the adapter bridges them into the sandbox over an MCP tool proxy that checks a token:
import { z } from "zod";
import { chat, toolDefinition } from "@tanstack/ai";
import { grokBuildText } from "@tanstack/ai-grok-build";
import { withSandbox } from "@tanstack/ai-sandbox";
import { sandbox } from "./sandbox";
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: grokBuildText("grok-build"),
messages: [{ role: "user", content: "What's the status of ticket T-123?" }],
threadId: "thread-1",
tools: [lookupTicket],
middleware: [withSandbox(sandbox)],
});For how the bridge reaches your server from each provider (local, Docker, or cloud), see Sandbox Tools.
You want Grok Build to see only one of your tools, or none of them. Pass toolChoice to chat():
const onlyLookup = chat({
adapter: grokBuildText("grok-build"),
messages: [{ role: "user", content: "What's the status of ticket T-123?" }],
threadId: "thread-1",
tools: [lookupTicket],
toolChoice: { type: "tool", name: "lookup_ticket" },
middleware: [withSandbox(sandbox)],
});toolChoice 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.
| Value | What 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.
You want a typed report after the agent reads the repo. Pass outputSchema on the same chat() call:
import { chat } from "@tanstack/ai";
import { grokBuildText } from "@tanstack/ai-grok-build";
import { withSandbox } from "@tanstack/ai-sandbox";
import { z } from "zod";
import { sandbox } from "./sandbox";
const Report = z.object({
summary: z.string(),
filesChanged: z.array(z.string()),
});
const report = await chat({
adapter: grokBuildText("grok-build"),
messages: [{ role: "user", content: "Review this repo." }],
threadId: "thread-1",
outputSchema: Report,
middleware: [withSandbox(sandbox)],
});
console.log(report.summary);On 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()),
});
export function ReportView() {
const { final, isLoading } = useChat({
connection: fetchServerSentEvents("/api/repo-report"),
threadId: "support-chat",
outputSchema: Report,
});
if (isLoading) return <p>The agent is inspecting the repo.</p>;
if (!final) return null;
return <p>{final.summary}</p>;
}Do you only need JSON from a prompt, with no repo? Use @tanstack/ai-grok. For the full walkthrough, see Harness Agents.
| Model id | Notes |
|---|---|
| grok-build | The short alias. With a grok.com browser login, the CLI lists the model under this name. |
| grok-build-0.1 | The full id. The CLI lists the model under this name when it signs in with XAI_API_KEY. |
| composer-2.5 | Also runs through the Grok Build harness. |
The adapter changes grok-build to grok-build-0.1 for the CLI, so the same code works with both ways to sign in. The package exports the list as GROK_BUILD_MODELS. The adapter also takes any other xAI model id as a string.
Pass these as the second argument of grokBuildText:
| Option | What it does |
|---|---|
| cwd | The working directory inside the sandbox. Default /workspace. |
| authMode | 'api-key' (default) signs in with XAI_API_KEY. 'host' uses grok login. See Harness Auth. |
| authMethodId | An explicit ACP auth method. It wins over authMode. |
| protocol | 'acp' (default) or 'streaming-json'. See Protocols and durable runs. |
| transport | The ACP transport: 'auto' (default), 'stdio', or 'websocket'. |
| acpPort | The port of grok agent serve in the sandbox, for the WebSocket transport. |
| permissionMode | The ACP permission policy: 'default', 'acceptEdits', or 'bypassPermissions' (default). |
| emitDiff | Send the file.changed event with the git diff after the run. Default true. |
| grokExecutable | The path or name of the grok executable in the sandbox. Default grok. |
| env | More environment variables for the grok process. |
| extraArgs | More raw CLI flags. The adapter adds them as they are. |
Pass these in modelOptions on chat(). They override the adapter options for one call:
The adapter talks to grok over one of two protocols:
A durable sandbox run (you pass runs and durability to withSandbox) writes the agent output to a journal, so a later request can take the run over. ACP does not write a journal. So 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:
See Durable Runs and The Run Journal.
On the 'streaming-json' path with auto-approve, the adapter adds --always-approve --no-plan --no-auto-update. These flags stop Plan Mode and the CLI update check from blocking a headless run.
For any other ACP agent, use the ACP-Compatible Harness.