Claude Agent SDK: Build Your Own AI Terminal in 10 Minutes
๐ป<br>Claude Agent SDK: Build Your Own AI Terminal in 10 Minutes โฌ๏ธ
~/mager.co/tech $ cat article.md<br>โ mager.co / March 14, 2026<br>Claude Agent SDK: Build Your Own AI Terminal in 10 Minutes<br>The Claude Agent SDK gives you the same engine that powers Claude Code, fully programmable. Here's how to build a custom TUI with it in 10 minutes.<br>AIAgentsClaudeTypeScriptTUITerminal
You've used Claude Code from the terminal. Now build your own.
That's the pitch for the Claude Agent SDK โ same engine that powers Claude Code, but programmable. You get the full agent loop โ file reading, bash execution, web search, code editing โ wrapped in a for await loop you control.
The question everyone asks: why would I use this instead of just calling the Claude API directly?
The answer: you don't have to implement the tool loop yourself.
And the most compelling use case for that? Building your own TUI.
Demo repo: github.com/mager/claude-tui-demo โ clone it and follow along.
The SDK vs. The API: What's the Actual Difference
With the standard Anthropic client SDK, you implement tool execution yourself:
// You write this loop. Every time.<br>let response = await client.messages.create({ ...params });<br>while (response.stop_reason === "tool_use") {<br>const result = yourToolExecutor(response.tool_use);<br>response = await client.messages.create({ tool_result: result, ...params });<br>With the Agent SDK:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({<br>prompt: "Find and fix the bug in auth.ts",<br>options: { allowedTools: ["Read", "Edit", "Bash"] }<br>})) {<br>console.log(message);<br>Claude reads the file, finds the bug, edits it. You stream the output. No tool loop, no executor, no boilerplate.
Built-in tools you get for free:
ToolWhat it doesReadRead any fileWriteCreate filesEditPrecise editsBashRun commands, git opsGlobFind files by patternGrepRegex file searchWebSearchSearch the webWebFetchFetch + parse URLs<br>That's Claude Code's entire toolset, programmable.
Why a TUI?
The Claude Code CLI is great for general use. But the moment you have a specific domain โ a codebase with custom conventions, a workflow with specialized steps, a team with different permission needs โ you want your own interface.
A custom TUI lets you:
Pre-load context your team cares about (architecture docs, style guides)
Lock down tools โ a read-only reviewer can't accidentally edit prod
Surface domain-specific shortcuts โ one keystroke to run your whole test suite
Pipe output into your CI/CD or logging infrastructure
Add hooks โ audit every file change, block destructive operations, require approval
You're not replacing Claude Code. You're building the version of Claude Code that fits your workflow exactly.
Let's Build It
Clone the demo and install:
git clone https://github.com/mager/claude-tui-demo.git<br>cd claude-tui-demo<br>npm install<br>export ANTHROPIC_API_KEY=your-key
API credits: You'll need an Anthropic API key with credits. Top up at platform.claude.com/settings/billing.
I'm using Ink for the terminal UI. If you know React, you already know Ink โ same component model, same hooks (useState, useEffect), same JSX. But instead of rendering to the DOM, it renders to your terminal. Box is your div. Text is your span. Flexbox and colors work exactly as you'd expect. It's the cleanest way to build interactive terminal UIs in TypeScript.
Note on the runner: The demo uses tsx instead of ts-node. tsx is zero-config โ it handles .tsx, JSX, and ESM out of the box without loader flags. Also make sure "type": "module" is in your package.json โ Ink's layout engine (yoga-layout) uses top-level await, which requires ESM mode. You'll hit a cryptic error without it.
Step 1: The Message Stream
// agent.ts<br>import { query } from "@anthropic-ai/claude-agent-sdk";
export async function* runAgent(prompt: string) {<br>for await (const message of query({<br>prompt,<br>options: {<br>allowedTools: ["Read", "Glob", "Grep", "Bash"],<br>},<br>})) {<br>yield message;
What's async function*? The * makes this a generator function โ instead of computing everything and returning at once, it hands you one value at a time via yield, pausing between each. async means it can also await internally. On the consumer side, for await handles the async stream one message at a time. This is how the tool calls and responses stream to your UI as they happen, not after everything finishes.
Step 2: The TUI Component
Ink gives us React-style components for the terminal. Box handles layout, Text handles output with color and style support.
// App.tsx<br>import React, { useState, useEffect } from "react";<br>import { Box, Text, useInput, useApp } from "ink";<br>import { runAgent } from "./agent.js";
type LogLine = { type: "user" | "agent" | "tool" | "result"; text: string };
function formatToolCall(block: any): string {<br>return `โ ${block.name}(${JSON.stringify(block.input).slice(0, 60)})`;
function handleAssistantMessage(msg: any,...