{"slug": "claude-agent-sdk-build-your-own-ai-terminal-in-10-minutes", "title": "Claude Agent SDK: Build Your Own AI Terminal in 10 Minutes", "summary": "Anthropic released the Claude Agent SDK, providing the same engine that powers Claude Code as a programmable tool for developers. The SDK allows users to build custom terminal interfaces in about 10 minutes, handling the agent loop for file reading, bash execution, web search, and code editing without requiring developers to implement tool execution themselves.", "body_md": "# Claude Agent SDK: Build Your Own AI Terminal in 10 Minutes\n\nThe 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.\n\nYou've used Claude Code from the terminal. Now build your own.\n\nThat'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`\n\nloop you control.\n\nThe question everyone asks: *why would I use this instead of just calling the Claude API directly?*\n\nThe answer: **you don't have to implement the tool loop yourself.**\n\nAnd the most compelling use case for that? Building your own TUI.\n\nDemo repo:[github.com/mager/claude-tui-demo]— clone it and follow along.\n\n## The SDK vs. The API: What's the Actual Difference\n\nWith the standard Anthropic client SDK, you implement tool execution yourself:\n\n``` js\n// You write this loop. Every time.\nlet response = await client.messages.create({ ...params });\nwhile (response.stop_reason === \"tool_use\") {\n  const result = yourToolExecutor(response.tool_use);\n  response = await client.messages.create({ tool_result: result, ...params });\n}\n```\n\nWith the Agent SDK:\n\n``` js\nimport { query } from \"@anthropic-ai/claude-agent-sdk\";\n\nfor await (const message of query({\n  prompt: \"Find and fix the bug in auth.ts\",\n  options: { allowedTools: [\"Read\", \"Edit\", \"Bash\"] }\n})) {\n  console.log(message);\n}\n```\n\nClaude reads the file, finds the bug, edits it. You stream the output. No tool loop, no executor, no boilerplate.\n\nBuilt-in tools you get for free:\n\n| Tool | What it does |\n|---|---|\n`Read` | Read any file |\n`Write` | Create files |\n`Edit` | Precise edits |\n`Bash` | Run commands, git ops |\n`Glob` | Find files by pattern |\n`Grep` | Regex file search |\n`WebSearch` | Search the web |\n`WebFetch` | Fetch + parse URLs |\n\nThat's Claude Code's entire toolset, programmable.\n\n## Why a TUI?\n\nThe 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.\n\nA custom TUI lets you:\n\n**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\n\nYou're not replacing Claude Code. You're building the version of Claude Code that fits your workflow exactly.\n\n## Let's Build It\n\nClone the demo and install:\n\n```\ngit clone https://github.com/mager/claude-tui-demo.git\ncd claude-tui-demo\nnpm install\nexport ANTHROPIC_API_KEY=your-key\n```\n\nAPI credits:You'll need an Anthropic API key with credits. Top up at[platform.claude.com/settings/billing].\n\nI'm using ** Ink** for the terminal UI. If you know React, you already know Ink — same component model, same hooks (\n\n`useState`\n\n, `useEffect`\n\n), same JSX. But instead of rendering to the DOM, it renders to your terminal. `Box`\n\nis your `div`\n\n. `Text`\n\nis your `span`\n\n. Flexbox and colors work exactly as you'd expect. It's the cleanest way to build interactive terminal UIs in TypeScript.\n\nNote on the runner:The demo uses`tsx`\n\ninstead of`ts-node`\n\n.`tsx`\n\nis zero-config — it handles`.tsx`\n\n, JSX, and ESM out of the box without loader flags. Also make sure`\"type\": \"module\"`\n\nis in your`package.json`\n\n— Ink's layout engine (`yoga-layout`\n\n) uses top-level`await`\n\n, which requires ESM mode. You'll hit a cryptic error without it.\n\n### Step 1: The Message Stream\n\n``` js\n// agent.ts\nimport { query } from \"@anthropic-ai/claude-agent-sdk\";\n\nexport async function* runAgent(prompt: string) {\n  for await (const message of query({\n    prompt,\n    options: {\n      allowedTools: [\"Read\", \"Glob\", \"Grep\", \"Bash\"],\n    },\n  })) {\n    yield message;\n  }\n}\n```\n\nWhat'sThe`async function*`\n\n?`*`\n\nmakes this agenerator function— instead of computing everything and returning at once, it hands you one value at a time via`yield`\n\n, pausing between each.`async`\n\nmeans it can also`await`\n\ninternally. On the consumer side,`for await`\n\nhandles 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.\n\n### Step 2: The TUI Component\n\n[Ink](https://github.com/vadimdemedes/ink) gives us React-style components for the terminal. `Box`\n\nhandles layout, `Text`\n\nhandles output with color and style support.\n\n``` python\n// App.tsx\nimport React, { useState, useEffect } from \"react\";\nimport { Box, Text, useInput, useApp } from \"ink\";\nimport { runAgent } from \"./agent.js\";\n\ntype LogLine = { type: \"user\" | \"agent\" | \"tool\" | \"result\"; text: string };\n\nfunction formatToolCall(block: any): string {\n  return `⚙ ${block.name}(${JSON.stringify(block.input).slice(0, 60)})`;\n}\n\nfunction handleAssistantMessage(msg: any, setLines: React.Dispatch<React.SetStateAction<LogLine[]>>) {\n  for (const block of msg.message.content) {\n    if (block.type === \"text\") {\n      setLines((prev) => [...prev, { type: \"agent\", text: block.text }]);\n    }\n    if (block.type === \"tool_use\") {\n      setLines((prev) => [...prev, { type: \"tool\", text: formatToolCall(block) }]);\n    }\n  }\n}\n\nexport function App({ prompt }: { prompt: string }) {\n  const [lines, setLines] = useState<LogLine[]>([]);\n  const [done, setDone] = useState(false);\n  const { exit } = useApp();\n\n  useEffect(() => {\n    setLines([{ type: \"user\", text: `> ${prompt}` }]);\n\n    (async () => {\n      for await (const msg of runAgent(prompt)) {\n        if (msg.type === \"assistant\") handleAssistantMessage(msg, setLines);\n        if (msg.type === \"result\") {\n          setLines((prev) => [...prev, { type: \"result\", text: `✓ ${msg.result}` }]);\n          setDone(true);\n        }\n      }\n    })();\n  }, []);\n\n  useInput((_, key) => {\n    if (key.escape || (key.ctrl && _.toLowerCase() === \"c\")) exit();\n  });\n\n  const colors: Record<LogLine[\"type\"], string> = {\n    user: \"cyan\",\n    agent: \"white\",\n    tool: \"yellow\",\n    result: \"green\",\n  };\n\n  return (\n    <Box flexDirection=\"column\" padding={1}>\n      <Box marginBottom={1}>\n        <Text bold color=\"cyan\">◆ My AI Terminal</Text>\n        <Text color=\"gray\">  (esc to quit)</Text>\n      </Box>\n      {lines.map((line, i) => (\n        <Text key={i} color={colors[line.type]}>{line.text}</Text>\n      ))}\n      {!done && <Text color=\"gray\">▸ thinking...</Text>}\n    </Box>\n  );\n}\n```\n\nMessage types:The SDK streams several message types —`assistant`\n\n(Claude's response),`result`\n\n(final outcome),`system`\n\n(init event with the session ID), and`user`\n\n(echoed input). Log`msg.type`\n\nduring development to see everything flowing through.\n\n### Step 3: The Entry Point\n\n``` python\n// index.tsx\nimport React from \"react\";\nimport { render } from \"ink\";\nimport { App } from \"./App.js\";\n\nconst prompt = process.argv.slice(2).join(\" \") || \"What files are in this directory?\";\n\nrender(<App prompt={prompt} />);\n```\n\n`process.argv.slice(2)`\n\ngrabs everything after `node`\n\nand the script path — your actual typed arguments. `.join(\" \")`\n\nreassembles multi-word prompts. Seven lines. That's the whole entry point.\n\nRun it:\n\n```\nnpm start \"What files are in this directory?\"\n```\n\nYou'll see Claude's tool calls stream in real-time — `⚙ Bash({\"command\":\"ls\"})`\n\nin yellow, the response in white, `✓ done`\n\nin green. That's a working AI TUI in ~80 lines.\n\n## Level Up: The Forever Loop (REPL Mode)\n\nThe single-prompt TUI is great for one-shot tasks. But what if you want Claude to just... keep responding? Like the real Claude Code experience — type a prompt, get a response, type another?\n\nThat's a REPL, and it's a `while (true)`\n\nloop:\n\n``` js\n// repl.ts\nimport { query } from \"@anthropic-ai/claude-agent-sdk\";\nimport * as readline from \"readline\";\n\nlet sessionId: string | undefined;\n\nconst rl = readline.createInterface({ input: process.stdin, output: process.stdout });\nconst ask = (prompt: string) => new Promise<string>((resolve) => rl.question(prompt, resolve));\n\nasync function runTurn(userPrompt: string) {\n  for await (const msg of query({\n    prompt: userPrompt,\n    options: { allowedTools: [\"Read\", \"Glob\", \"Grep\", \"Bash\"], resume: sessionId },\n  })) {\n    if (msg.type === \"system\" && msg.subtype === \"init\") sessionId = msg.session_id;\n    if (msg.type === \"assistant\") {\n      for (const block of msg.message.content) {\n        if (block.type === \"text\") process.stdout.write(`\\n🤖 ${block.text}\\n`);\n        if (block.type === \"tool_use\") {\n          process.stdout.write(`⚙  ${block.name}(${JSON.stringify(block.input).slice(0, 80)})\\n`);\n        }\n      }\n    }\n  }\n}\n\nconsole.log(\"◆ Claude REPL — type your prompt, ctrl+c to quit\\n\");\n\nwhile (true) {\n  const input = await ask(\"\\n> \");\n  if (!input.trim()) continue;\n  await runTurn(input.trim());\n}\n```\n\nRun it with `npm run repl`\n\n. Type anything. Claude responds. Type again — it still has context from everything before. That's the `resume: sessionId`\n\ndoing its job.\n\n## Level Up: Hooks\n\nThe real power is hooks — callbacks that fire at key points in the agent lifecycle. This is how you add audit logs, approval gates, or custom UI feedback:\n\n``` js\n// agent.ts (with hooks)\nimport { query } from \"@anthropic-ai/claude-agent-sdk\";\nimport { appendFile } from \"fs/promises\";\n\nconst auditHook = async (input: any) => {\n  const tool = input.tool_name ?? \"unknown\";\n  const args = JSON.stringify(input.tool_input ?? {}).slice(0, 100);\n  await appendFile(\"./audit.log\", `${new Date().toISOString()}  ${tool}  ${args}\\n`);\n  return {};\n};\n\nfor await (const message of query({\n  prompt: \"Refactor the auth module\",\n  options: {\n    allowedTools: [\"Read\", \"Edit\", \"Bash\"],\n    hooks: {\n      PostToolUse: [{ matcher: \".*\", hooks: [auditHook] }],\n    },\n  },\n})) {\n  // render to your TUI\n}\n```\n\nEvery tool call gets logged to `audit.log`\n\nwith a timestamp. `matcher: \".*\"`\n\ncatches everything — narrow to `\"Edit|Write\"`\n\nif you only care about mutations.\n\nOther hooks worth knowing: `PreToolUse`\n\nto block operations before they run, `Stop`\n\nto detect when the agent finishes, `UserPromptSubmit`\n\nto pre-process or validate input.\n\n## Level Up: Persistent Sessions\n\nThe agent remembers context across multiple `query()`\n\ncalls. Capture the session ID from the first run, pass it to the next:\n\n``` js\nlet sessionId: string | undefined;\n\n// First turn — Claude reads the file\nfor await (const msg of query({ prompt: \"Read the auth module\" })) {\n  if (msg.type === \"system\" && msg.subtype === \"init\") {\n    sessionId = msg.session_id;\n  }\n}\n\n// Second turn — zero tool calls, Claude already knows\nfor await (const msg of query({\n  prompt: \"Now find everything that calls it\",\n  options: { resume: sessionId },\n})) {\n  // ...\n}\n```\n\nThe money detail: the second turn fires **zero tool calls** — Claude already has the file in context. No re-reading, no extra API calls. It just answers.\n\nRun the demo: `npm run session`\n\n.\n\n## Bonus: Level Up with Rezi\n\nInk is great for quick TUIs. But if you want richer widgets — tables, command palettes, split panes, charts, modals — ** Rezi** is the upgrade path. Still TypeScript, still Node.js, but native-backed rendering through a C engine and 50+ built-in widgets.\n\nWhere Ink feels like React (hooks, JSX, component tree), Rezi is state-driven: you define a `view`\n\nfunction that maps state → UI, and call `app.update()`\n\nto change state. Same mental model as Bubble Tea's Elm Architecture, but in TypeScript.\n\nInstall it:\n\n```\nnpm install @rezi-ui/core @rezi-ui/node\n```\n\nHere's the same Claude TUI rebuilt in Rezi:\n\n``` js\n// rezi/rezi-app.ts\nimport { ui } from \"@rezi-ui/core\";\nimport { createNodeApp } from \"@rezi-ui/node\";\nimport { query } from \"@anthropic-ai/claude-agent-sdk\";\n\ntype LineKind = \"user\" | \"agent\" | \"tool\" | \"result\";\ntype LogLine = { kind: LineKind; text: string };\ntype State = { lines: LogLine[]; done: boolean };\n\nconst prompt = process.argv.slice(2).join(\" \") || \"What files are in this directory?\";\n\nconst app = createNodeApp<State>({\n  initialState: { lines: [{ kind: \"user\", text: `> ${prompt}` }], done: false },\n});\n\nconst kindVariant: Record<LineKind, string> = {\n  user: \"info\",\n  agent: \"body\",\n  tool: \"warning\",\n  result: \"success\",\n};\n\napp.view((state) =>\n  ui.page({\n    p: 1,\n    gap: 1,\n    header: ui.header({ title: \"◆ My AI Terminal\", subtitle: \"q to quit\" }),\n    body: ui.panel(\"Output\", [\n      ...state.lines.map((line, i) =>\n        ui.text(line.text, { key: String(i), variant: kindVariant[line.kind] as any })\n      ),\n      ...(!state.done ? [ui.spinner({ label: \"thinking…\", key: \"spinner\" })] : []),\n    ]),\n  })\n);\n\napp.keys({ q: () => app.stop(), escape: () => app.stop() });\n\n// Kick off the agent stream\n(async () => {\n  for await (const msg of query({\n    prompt,\n    options: { allowedTools: [\"Read\", \"Glob\", \"Grep\", \"Bash\"] },\n  })) {\n    if (msg.type === \"assistant\") {\n      for (const block of msg.message.content) {\n        if (block.type === \"text\") {\n          app.update((s) => ({ ...s, lines: [...s.lines, { kind: \"agent\", text: block.text }] }));\n        }\n        if (block.type === \"tool_use\") {\n          const preview = JSON.stringify(block.input).slice(0, 60);\n          app.update((s) => ({\n            ...s,\n            lines: [...s.lines, { kind: \"tool\", text: `⚙ ${block.name}(${preview})` }],\n          }));\n        }\n      }\n    }\n    if (msg.type === \"result\") {\n      app.update((s) => ({\n        ...s,\n        lines: [...s.lines, { kind: \"result\", text: `✓ ${msg.result}` }],\n        done: true,\n      }));\n    }\n  }\n})();\n\nawait app.start();\n```\n\nThe key differences from the Ink version:\n\n**No React**—`app.view()`\n\nis a pure function of state, not a component tree**No**— the agent stream runs outside the view;`useEffect`\n\n`app.update()`\n\npushes state changes inbuilt in — no manual blinking text`ui.spinner()`\n\n**Semantic variants**(`info`\n\n,`warning`\n\n,`success`\n\n) — Rezi handles the colors per-theme\n\nThe full Rezi version lives in [ rezi/rezi-app.ts](https://github.com/mager/claude-tui-demo/tree/main/rezi) in the demo repo:\n\n```\ncd rezi\nnpm install\nexport ANTHROPIC_API_KEY=your-key\nnpm start \"What files are in this directory?\"\n```\n\n**Ink vs Rezi at a glance:**\n\n| Ink | Rezi | |\n|---|---|---|\n| Mental model | React hooks + JSX | State-driven, pure view fn |\n| State | `useState` | `app.update()` |\n| Side effects | `useEffect` | Run outside the view |\n| Styling | Color/bold props | Semantic variants + 6 built-in themes |\n| Widget library | Minimal (Text, Box) | 50+ (tables, modals, charts, command palette) |\n| Rendering | Node.js | Native C engine via Zireael |\n| Best for | Quick TUIs, React devs | Production tools, rich UIs |\n\nBoth work perfectly with the Agent SDK stream. Ink is the fastest on-ramp; Rezi is where you go when you outgrow it.\n\n## Real-World Example: The Email Agent\n\nAnthropic ships a reference implementation of this pattern — an [email agent](https://github.com/anthropics/claude-agent-sdk-demos/tree/main/email-agent) that reads your inbox, drafts replies, and sends them. It's a great study in how hooks + persistent sessions compose in production: `PreToolUse`\n\nto require approval before sending, `PostToolUse`\n\nto log every action, session resume to maintain context across a multi-step triage workflow. The same ~80-line skeleton we just built, extended into something genuinely useful.\n\n## When to Use the SDK vs. the CLI\n\n| Scenario | Use |\n|---|---|\n| Daily development, one-off tasks | Claude Code CLI |\n| CI/CD pipelines | SDK |\n| Custom team tools | SDK |\n| Domain-specific workflows | SDK |\n| Production automation | SDK |\n| Audit trails + permission control | SDK |\n\nThe workflows translate directly. Anything Claude Code can do in the CLI, the SDK can do programmatically.\n\n## The Bigger Picture\n\nThe Agent SDK is a general-purpose agent runtime — not just a coding tool. The built-in tools, the hooks system, the subagent delegation, the MCP support — it's a full agent platform.\n\nThe TUI is just one entry point. You could build:\n\n- A Slack bot where Claude actually edits your codebase\n- A CI/CD step that auto-fixes lint errors before merging\n- An internal tool where junior devs prompt in plain English and senior devs approve tool calls\n- A research agent with web search and file output\n\nThe pattern is always the same: `for await (const message of query(...))`\n\n. Stream it, render it, hook into it.\n\nExplore further:\n\n**Demo repo:**[github.com/mager/claude-tui-demo](https://github.com/mager/claude-tui-demo)** Docs:**[platform.claude.com/docs/en/agent-sdk/overview](https://platform.claude.com/docs/en/agent-sdk/overview)** Reference agents:**[github.com/anthropics/claude-agent-sdk-demos](https://github.com/anthropics/claude-agent-sdk-demos)** Python SDK:**`pip install claude-agent-sdk`\n\nThe terminal isn't going anywhere. Might as well make it yours.", "url": "https://wpnews.pro/news/claude-agent-sdk-build-your-own-ai-terminal-in-10-minutes", "canonical_source": "https://www.mager.co/blog/2026-03-14-claude-agent-sdk-tui/", "published_at": "2026-05-31 10:52:42+00:00", "updated_at": "2026-05-31 11:16:48.871157+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-products", "large-language-models", "generative-ai"], "entities": ["Claude Agent SDK", "Claude Code", "Anthropic", "Claude"], "alternates": {"html": "https://wpnews.pro/news/claude-agent-sdk-build-your-own-ai-terminal-in-10-minutes", "markdown": "https://wpnews.pro/news/claude-agent-sdk-build-your-own-ai-terminal-in-10-minutes.md", "text": "https://wpnews.pro/news/claude-agent-sdk-build-your-own-ai-terminal-in-10-minutes.txt", "jsonld": "https://wpnews.pro/news/claude-agent-sdk-build-your-own-ai-terminal-in-10-minutes.jsonld"}}