A deep dive into how Anthropic's CLI agent is built — layers, data flow, and design decisions.
Single-package Bun-compiled · React Terminal UI · Async Generator Agent LoopKey numbers and characteristics of the codebase
Click each layer to expand details. Each layer only depends on layers below it.
Stock Ink can't handle the interaction complexity needed — text selection, transcript search, mouse hit-testing, and virtual scrolling of large conversation histories all require deep control of the terminal rendering pipeline.
Key innovations: double-buffered screen rendering with cell-level diffing, React Compiler for auto-memoized components, Yoga (flexbox) layout engine, and self-rendering tools (each tool defines its own React components for display).
screens/REPL.tsxcomponents/ink/
ink/reconciler.tsink/screen.tsink/selection.ts
The async generator pattern is key: query() yields stream events that the REPL consumes for rendering, while the headless QueryEngine consumes the same events for SDK/programmatic use. Same loop, two consumers.
The permission gateway (useCanUseTool) sits here as a cross-cutting concern, intercepting every tool call before it reaches the execution layer.
query.tsQueryEngine.ts
hooks/useCanUseTool.tsxquery/
Read-only tools like Read, Glob, Grep run in parallel (up to 10). Mutating tools like Edit, Bash run serially. This partition maximizes throughput while preserving correctness.
Each tool execution follows: validate input (Zod) → check permissions → run pre-hooks → execute → run post-hooks → budget output.
services/tools/toolOrchestration.ts
services/tools/toolExecution.ts
services/tools/toolHooks.ts
The LLM client supports Anthropic direct, AWS Bedrock, and GCP Vertex. It manages prompt caching (1h TTL), retry logic, token budgets, and auto-continuation when output is exhausted.
Context compaction proactively summarizes conversation history before hitting limits, with reactive fallback on prompt_too_long errors.
services/api/claude.tsservices/mcp/client.ts
services/compact/services/api/withRetry.ts
bootstrap/state.ts is a deliberate singleton holding session ID, model, API key, and flags — avoids threading config through every function call.
entrypoints/init.ts runs the initialization sequence: config loading, environment variables, graceful shutdown handlers, telemetry setup, and proxy configuration.
bootstrap/state.tsentrypoints/init.ts
utils/state/AppStateStore.ts
From binary execution to the interactive REPL — a two-stage entry for fast cold starts
entrypoints/cli.tsx — handles fast paths (--version, --dump-system-prompt) with zero module imports for instant response. Only proceeds to heavy initialization when needed.
main.tsx (~2000 lines) — Commander.js CLI definition, parallel prefetches (MDM settings, keychain), flag parsing, and init() call for config/env/telemetry.
Connects to all configured MCP servers via getMcpToolsCommandsAndResources(). Initializes permission context and runs 10+ migration scripts.
Interactive: launchRepl() → REPL.tsx via React/Ink rendering loop.
Headless: -p flag → QueryEngine.ts directly (no UI).
React component tree mounts. REPL.tsx manages message state, prompt input, permission dialogs, background tasks, and the query loop.
An async generator implementing the classic LLM agent pattern — stream, act, observe, repeat
The REPL screen consumes yielded StreamEvents to render tokens in real-time. Permission prompts show as UI dialogs. Tool results render inline with custom components.
QueryEngine.ts consumes the same async generator without a UI. Used for -p flag, SDK integrations, and programmatic access. Same loop, different consumer.
Every tool implements the Tool<Input, Output, Progress> interface. Self-describing, self-rendering, permission-aware.
Read-only tools: Read, Glob, Grep, WebFetch. Safe to parallelize because they don't mutate state.
Mutating tools: Edit, Write, Bash, Agent. Must run one at a time to prevent race conditions.
Layered security: mode → rules → tool-specific checks → user confirmation
| Mode | Behavior | Risk Level | Use Case |
|---|---|---|---|
| default | Prompts for most operations | Safe | Normal interactive use |
| acceptEdits | Auto-approves file edits, prompts for bash | Safe | Trusted editing sessions |
| plan | Read-only — no writes permitted | Safe | Architecture review / exploration |
| auto | AI classifier auto-approves based on patterns | Medium | Autonomous agentic work |
| bypassPermissions | Skips all permission prompts | High | CI/CD, sandboxed environments |
Model Context Protocol — extending Claude Code with external tool servers
MCPTool
MCP tools are normalized to API-safe names: mcp__<server>__<tool>
Example: Figma's get_design_context becomes mcp__claude_ai_Figma__get_design_context
buildMcpToolName() constructs them; mcpInfoFromString() parses them back.
Color-coded by architectural layer
The "why" behind the architecture
Uses a heavily forked Ink framework to render React components in the terminal. Enables declarative, component-based UI with the same mental model as web development.
query() is an async generator that yields stream events. The REPL and SDK consume the same generator differently.
Bun's feature() macro replaces flags with boolean literals at compile time, enabling dead-code elimination.
Every tool (built-in, MCP, agent) implements the same Tool<I, O, P> interface with schema, permissions, rendering.
Rather than embedding checks inside tools, the permission gateway sits at the orchestration layer and applies uniformly to all tool calls.
cli.tsx handles fast paths with zero imports. Heavy initialization only loads when actually needed.
--version responds instantly. Cold start for the full REPL is optimized with parallel prefetches (keychain, MDM settings).Automatically summarizes conversation history before hitting context limits, with a reactive fallback on prompt_too_long errors.
bootstrap/state.ts holds session ID, model, API key, and flags as a deliberate mutable singleton.