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.tsx→replLauncher.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.ts→query.ts. Exposes an async generator interface yielding strongly-typedSDKMessageevents for programmatic embedding.
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:
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.
Key bridge files and what they do:
| File | Size | Responsibility |
|---|---|---|
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).
Component Interaction Flow
In the REPL, an Ink React tree manages all rendering. The key components and their interactions are:
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:
| Category | Notable examples |
|---|---|
| Invented / nonsense | Flibbertigibbeting, Whatchamacalliting, Discombobulating, Recombobulating, Razzle-dazzling, Razzmatazzing, Hullaballooing, Boondoggling, Fiddle-faddling |
| Self-referential | Clauding, Gitifying, Newspapering |
| Scientific | Photosynthesizing, Nucleating, Osmosing, Sublimating, Nebulizing, Fermenting, Ionizing |
| Dance / movement | Beboppin', Boogieing, Moonwalking, Jitterbugging, Sock-hopping, Shimmy-ing, Grooving |
| Culinary | Baking, Brewing, Caramelizing, Flambéing, Julienning, Sautéing, Tempering, Zesting |
| Nature | Billowing, 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:
- Input capture: Ink's
PromptInputcaptures the text and submits touseCommandQueue. - Slash command check:
processUserInput()determines this is not a slash command; creates aUserMessagewith the text. - Context assembly:
query.tscallsfetchSystemPromptParts(), loads any relevant CLAUDE.md memory, and prepends user context. - API request: Messages + system prompt + tool schemas sent to Claude API as a streaming request.
- Stream parse: As the stream arrives, text blocks render immediately in the terminal. When a
tool_useblock appears forFileEditTool, dispatch begins. - Permission check:
canUseTool()evaluates the edit against allow/deny rules. In default mode, shows a diff and prompts "Apply this edit? [y/n]". - Tool execution:
FileEditTool.call()applies the edit and returns aToolResult. - 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.
- 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?
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?
~/.claude/settings.json file using the spinnerVerbs configuration hook.