System Architecture

A comprehensive look at how Claude Code is structured: from the top-level directory layout through the main event loop, component interactions, and state management flow.

High-Level Overview

Claude Code is architected as two interleaved execution paths sharing a common core:

  • Interactive REPL path: Driven by main.tsxreplLauncher.tsx → Ink React components + query.ts. All user-facing rendering, keyboard handling, and real-time progress is in this path.
  • Headless / SDK path: Driven by main.tsx (print/SDK mode) → QueryEngine.tsquery.ts. Exposes an async generator interface yielding strongly-typed SDKMessage events for programmatic embedding.
main.tsx CLI entry point · startup side effects · arg parse REPL MODE replLauncher.tsx Ink React tree real-time render · keyboard events SDK / PRINT MODE QueryEngine.ts AsyncGenerator<SDKMessage> headless · programmatic embedding shared core query.ts agentic loop · stream parse · tool dispatch ANTHROPIC API streaming SSE · prompt caching · model selection TOOL DISPATCH BashTool · FileReadTool · FileEditTool · GrepTool WebFetchTool · AgentTool · MCPTool · + 40 more

Directory Structure

The source root is nearly flat; subdirectories represent coherent subsystems rather than deep nesting. Below is the full annotated structure from the README, with file sizes for the most important entry points:

claude-code-src/ ├─ main.tsx # CLI entry point + main event loop (~4,683 lines) ├─ query.ts # Claude API query + tool execution loop (~1,729 lines) ├─ QueryEngine.ts # SDK-facing engine: state, compaction, API (~1,295 lines) ├─ commands.ts # All 80+ CLI command exports ├─ Tool.ts # Abstract tool interface and types (~792 lines) ├─ tools.ts # Tool registry and instantiation ├─ Task.ts # Task types and ID generation ├─ history.ts # Session history and input cache ├─ context.ts # System/user context assembly ├─ cost-tracker.ts # Token + dollar cost tracking ├─ setup.ts # Pre-init: Node checks, dirs, git, worktrees ├─ assistant/ # KAIROS assistant mode (feature-gated) ├─ bootstrap/ # Bootstrap state management ├─ bridge/ # Remote session bridge protocol (33 files) │ ├─ bridgeMain.ts # Main bridge orchestration (~115KB) │ ├─ replBridge.ts # REPL ↔ remote communication (~100KB) │ ├─ remoteBridgeCore.ts │ ├─ createSession.ts # Session provisioning via REST API │ └─ trustedDevice.ts # JWT-based device trust ├─ buddy/ # Companion sprite/animation system ├─ cli/ # Terminal I/O, structured output, transports ├─ commands/ # 80+ individual command implementations │ ├─ commit/ │ ├─ review/ │ ├─ mcp/ │ ├─ session/ │ ├─ autofix-pr/ │ ├─ bughunter/ │ └─ workflows/ ├─ components/ # 147 React/Ink terminal UI components │ ├─ design-system/ │ └─ agents/ ├─ constants/ # Product info, OAuth config, XML tags ├─ context/ # React context providers ├─ coordinator/ # Multi-agent coordinator mode ├─ entrypoints/ # cli.tsx, init.ts, mcp.ts, sdk/ ├─ hooks/ # 88 custom React hooks │ ├─ useCanUseTool.tsx │ ├─ useGlobalKeybindings.tsx │ └─ useIDEIntegration.tsx ├─ ink/ # Terminal rendering (Ink wrapper, 51 files) ├─ keybindings/ # Keyboard shortcut configuration ├─ memdir/ # Memory file management (CLAUDE.md) ├─ migrations/ # Database schema migrations ├─ native-ts/ # Native TypeScript runtime utilities ├─ outputStyles/ # Output formatting styles ├─ plugins/ # Plugin system + bundled plugins ├─ services/ # 39 backend service modules │ ├─ api/ # Claude API client, retry, rate limits │ ├─ mcp/ # MCP server management │ ├─ compact/ # Context compaction strategies │ ├─ analytics/ # GrowthBook, telemetry │ ├─ lsp/ # Language Server Protocol │ └─ oauth/ # OAuth token management ├─ skills/ # Skills system + 19 bundled skills │ └─ bundled/ │ ├─ update-config/ │ ├─ simplify/ │ ├─ loop/ │ └─ schedule/ ├─ state/ # Global Zustand-like app state ├─ tasks/ # Background task management ├─ tools/ # 46+ tool implementations │ ├─ BashTool/ │ ├─ FileEditTool/ │ ├─ FileReadTool/ │ ├─ FileWriteTool/ │ ├─ GlobTool/ │ ├─ GrepTool/ │ ├─ WebFetchTool/ │ ├─ WebSearchTool/ │ ├─ AgentTool/ │ ├─ SkillTool/ │ ├─ MCPTool/ │ ├─ EnterPlanModeTool/ │ ├─ AskUserQuestionTool/ │ └─ ScheduleCronTool/ ├─ types/ # Shared TypeScript types ├─ upstreamproxy/ # Upstream HTTP proxy ├─ utils/ # 332+ utility modules │ ├─ bash/ │ ├─ permissions/ │ ├─ settings/ │ └─ git.ts, auth.ts, config.ts ├─ vim/ # Vim keybinding mode └─ voice/ # Voice input/output (feature-gated)

Bridge Protocol: Remote Execution

The bridge/ directory implements one of the more complex subsystems: the ability for Claude Code to run in remote or sandboxed environments while the user interacts locally. This is what powers the web-based claude.ai/code interface.

LOCAL CLI bridgeMain.ts replBridge.ts REMOTE RUNNER Anthropic cloud infrastructure createSession() · REST API replBridge · WebSocket streaming results (return) trustedDevice.ts JWT-based device trust and auth canUseTool() still runs, no bypass BRIDGE_MODE feature flag dead-code eliminated in local builds Full Claude Code REPL on cloud infra

Key bridge files and what they do:

FileSizeResponsibility
bridgeMain.ts ~115KB Full bridge orchestration: manages the lifecycle of a remote session, proxies tool calls, handles reconnection logic
replBridge.ts ~100KB REPL message protocol over the WebSocket bridge, marshaling the interactive terminal session across the network
createSession.ts ~15KB Provisions a new remote session via Anthropic's REST API, returns connection credentials
trustedDevice.ts ~22KB JWT-based device trust: issues and validates device tokens so the remote runner can verify the local client

The bridge is enabled by the BRIDGE_MODE feature flag. In local CLI builds, this code is dead-code eliminated entirely. The remote bridge is how cloud-based code execution environments connect to the Claude Code REPL: the same terminal UI, running on Anthropic's infrastructure.

Main Event Loop

The heart of Claude Code is its agentic query loop, implemented in query.ts. The loop handles one "turn" at a time, a turn being the complete cycle from receiving a user message to producing a final response (or a sequence of tool-call/response pairs).

BOOTSTRAP PHASE Parse CLI args · load config · init auth Prefetch: keychain · MDM · GrowthBook · MCP URLs Build tool list (built-ins + MCP tools) · assemble system prompt INIT PHASE init(): trust dialog · telemetry · feature flags Connect MCP servers (parallel) · load plugins · agents · skills REPL / QUERY LOOP repeats per user message 1. processUserInput() Detect slash commands → route to handler (/compact, /review, /commit...) · or assemble API message 2. query() · inner agentic loop Normalize messages · fetch system prompt · attach memory / CLAUDE.md files ↳ API Stream: text blocks → render · tool_use blocks → dispatch Tool dispatch: checkPermissions() → canUseTool() → tool.call() → ToolResult → loop 3. Compaction check Token count approaching limit? → auto-compact or warn user 4. Yield to user → await next input CLEANUP PHASE flushSessionStorage() · close MCP connections · flush analytics

Component Interaction Flow

In the REPL, an Ink React tree manages all rendering. The key components and their interactions are:

App Root Ink component REPL SCREEN useCommandQueue useCanUseTool · useCancelRequest MessageList Renders each Message in history tool.renderToolUseMessage() PromptInput Captures keystrokes, submits to query PermissionDialog Appears when tool needs approval y / n / always / never / explain STATUS LINE token count model name · session cost TODO PANEL when task list active TodoWriteTool renders here not in chat transcript renders in sidebar, not message list

State Management

Claude Code uses a custom Zustand-inspired store pattern rather than Redux. The AppState type is a large immutable snapshot of all runtime state, mutated via a setAppState updater function passed through the component tree.

// AppState shape (simplified from state/AppState.tsx)
export type AppState = {
  // Conversation
  messages: Message[]
  permissionMode: PermissionMode

  // UI state
  toolJSX: React.ReactNode | null
  inProgressToolUseIDs: Set<string>
  responseLength: number
  streamMode: SpinnerMode

  // Tasks / subagents
  tasks: Map<AgentId, TaskState>
  backgroundTasks: BackgroundTaskState[]

  // Session
  sessionId: string
  fileHistory: FileHistoryState
  attribution: AttributionState

  // Notifications
  notifications: Notification[]
}

// Usage pattern in components
function MyComponent({ setAppState }) {
  const handleSomething = () => {
    setAppState(prev => ({
      ...prev,
      permissionMode: 'acceptEdits'
    }))
  }
}

The setAppState function is threaded all the way from the root Ink component down through ToolUseContext into every tool call. This allows tools to update UI state (e.g., show progress, add notifications) without direct component references. Subagents get a no-op version of setAppState by default, with a separate setAppStateForTasks callback for infrastructure that must survive across turns.

Message Type System

All conversation history is modeled as a discriminated union of Message types defined in types/message.ts. This allows exhaustive handling at every boundary:

// types/message.ts (simplified)
export type Message =
  | UserMessage           // User input, tool results
  | AssistantMessage      // Claude text + tool_use blocks
  | SystemMessage         // System messages (shown in REPL, stripped for API)
  | ToolUseSummaryMessage // Compact summary replacing tool exchanges
  | AttachmentMessage     // CLAUDE.md / memory files
  | ProgressMessage<T>   // Real-time tool progress (stripped before API)
  | TombstoneMessage      // Placeholder for deleted messages

// The normalizeMessagesForAPI() function strips all non-API-safe
// message types before sending to Claude, ensuring the API never
// sees UI-only messages like ProgressMessage or SystemLocalCommandMessage

Feature Flag Architecture

One of the most interesting architectural patterns is the use of Bun's dead-code elimination (DCE) via the feature() macro from bun:bundle. This allows entire subsystems to be conditionally compiled out of the production bundle:

// query.ts - conditional imports eliminated at build time
const reactiveCompact = feature('REACTIVE_COMPACT')
  ? require('./services/compact/reactiveCompact.js')
  : null

const contextCollapse = feature('CONTEXT_COLLAPSE')
  ? require('./services/contextCollapse/index.js')
  : null

// QueryEngine.ts - coordinator mode
const getCoordinatorUserContext = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js').getCoordinatorUserContext
  : () => ({})

// main.tsx - assistant/KAIROS mode
const assistantModule = feature('KAIROS')
  ? require('./assistant/index.js')
  : null

This pattern eliminates both the code size and the runtime import cost of experimental features in production builds. Because Bun evaluates feature() at bundle time, the dead branches are literally removed from the output, there are no unused module evaluations.

The Two-Path Architecture in Detail

Understanding the split between the REPL path and SDK path is essential to grasping the codebase structure:

Aspect REPL (Interactive) SDK (Headless)
Entry replLauncher.tsx QueryEngine.ts
Rendering Ink React components JSON/text output stream
State AppState via Ink hooks Internal class fields
User interaction Full keyboard input, dialogs Programmatic only
Permission prompts Interactive dialog UI Yielded as SDKPermissionDenial
Session persistence Always (configurable) Configurable via flag
Compaction User-visible warning + auto Automatic, transparent
Core loop query.ts (shared)

Source Deep Dive: spinnerVerbs.ts: Claude's Personality Layer

While the agentic loop is running and Claude is "thinking", the terminal shows a spinner with a randomly selected present-participle verb, things like "Calculating...", "Brewing...", "Flibbertigibbeting...". The source of all 190+ of these lives in src/prompts/spinnerVerbs.ts, and it's one of the most delightfully human corners of the entire codebase.

The Full Verb Collection

The list spans four alphabetical passes through the English language, with a few creative detours. Beyond the mundane ("Computing", "Processing", "Working"), the list includes culinary verbs ("Blanching", "Caramelizing", "Flambéing", "Julienning", "Sautéing", "Tempering"), motion verbs ("Gallivanting", "Lollygagging", "Moseying", "Perambulating", "Skedaddling"), and a handful of clearly invented words:

CategoryNotable examples
Invented / nonsenseFlibbertigibbeting, Whatchamacalliting, Discombobulating, Recombobulating, Razzle-dazzling, Razzmatazzing, Hullaballooing, Boondoggling, Fiddle-faddling
Self-referentialClauding, Gitifying, Newspapering
ScientificPhotosynthesizing, Nucleating, Osmosing, Sublimating, Nebulizing, Fermenting, Ionizing
Dance / movementBeboppin', Boogieing, Moonwalking, Jitterbugging, Sock-hopping, Shimmy-ing, Grooving
CulinaryBaking, Brewing, Caramelizing, Flambéing, Julienning, Sautéing, Tempering, Zesting
NatureBillowing, Blooming, Photosynthesizing, Pollinating, Germinating, Sprouting, Crystallizing

A few highlights worth calling out: "Clauding" is the only self-referential verb; the model is described as doing itself. "Beboppin'" (with the apostrophe) is the only entry with punctuation in the middle. "Flibbertigibbeting" holds the record for longest verb at 19 characters.

User Customization API

The verbs are not hardcoded for all users. The getSpinnerVerbs() function reads from settings first:

export function getSpinnerVerbs(): string[] {
  const settings = getInitialSettings()
  const config = settings.spinnerVerbs

  if (!config) {
    return SPINNER_VERBS  // No customization: use defaults
  }

  if (config.mode === 'replace') {
    // Replace entire list; fall back to defaults if list is empty
    return config.verbs.length > 0 ? config.verbs : SPINNER_VERBS
  }

  // Append mode: add custom verbs to the built-in list
  return [...SPINNER_VERBS, ...config.verbs]
}

You can add your own spinner verbs in ~/.claude/settings.json using two modes:

// Append your verbs to the built-in list:
{
  "spinnerVerbs": {
    "mode": "append",
    "verbs": ["Philosophizing", "Yodeling", "Overthrinking"]
  }
}

// Replace the entire list with your own:
{
  "spinnerVerbs": {
    "mode": "replace",
    "verbs": ["Thinking", "Processing"]
  }
}

If you set mode: "replace" with an empty array, the system silently falls back to the built-in list rather than showing a blank spinner, a small but thoughtful guard against misconfiguration. This customization hook is one of the few truly personality-facing extensibility points in the user settings schema.

End-to-End Data Flow

Here is a concrete trace of what happens when a user types "edit src/main.ts to fix the bug on line 42" in the REPL:

  1. Input capture: Ink's PromptInput captures the text and submits to useCommandQueue.
  2. Slash command check: processUserInput() determines this is not a slash command; creates a UserMessage with the text.
  3. Context assembly: query.ts calls fetchSystemPromptParts(), loads any relevant CLAUDE.md memory, and prepends user context.
  4. API request: Messages + system prompt + tool schemas sent to Claude API as a streaming request.
  5. Stream parse: As the stream arrives, text blocks render immediately in the terminal. When a tool_use block appears for FileEditTool, dispatch begins.
  6. Permission check: canUseTool() evaluates the edit against allow/deny rules. In default mode, shows a diff and prompts "Apply this edit? [y/n]".
  7. Tool execution: FileEditTool.call() applies the edit and returns a ToolResult.
  8. Loop continuation: The tool result is added to messages and a new API request is made. Claude may do more tool calls or produce the final response.
  9. Final render: When no more tool calls, the text response is fully rendered and the REPL awaits the next user input.

Frequently Asked Questions

Does Claude Code use Redux for state?
No. Claude Code uses a custom, lightweight, Zustand-inspired store pattern rather than Redux. This keeps state immutable and predictably threaded through the Ink component tree without the heavy boilerplate of Redux.
What is BRIDGE_MODE used for?
BRIDGE_MODE is a compile-time feature flag that powers Claude Code's remote execution architecture. In typical local builds, it is eliminated via dead-code elimination (DCE). In remote environments, it proxies the REPL across WebSockets.
Can I customize the "thinking" verb animations?
Yes! Claude Code includes over 190 built-in spinner verbs (like "Flibbertigibbeting..."). You can override or append to this list via your ~/.claude/settings.json file using the spinnerVerbs configuration hook.
Does Claude Code use a standard frontend framework?
It uses React alongside Ink to render a reactive component tree directly into the terminal, rather than a browser.
How is the codebase structured?
It is surprisingly flat. The most critical files like query.ts, main.tsx, and QueryEngine.ts live in the root directory rather than deep nested folders.
Can the SDK mode be used in other applications?
Yes, because of the two-path architecture, QueryEngine.ts acts as an async generator that can be headless and integrated into any environment programmatically.