Chapter 01: Project Overview and Architecture
What You'll Learn
By the end of this chapter, you will be able to:
- Describe Claude Code's overall architecture in one sentence, with enough precision to be useful when reading the source
- Navigate the 35 modules in
src/and locate any one of them by its functional role - Trace the complete data flow from a user's keypress to rendered output on screen
- Explain how feature flags eliminate code at both compile time and runtime, and why this matters for reading the codebase
What Is Claude Code?
Claude Code is a CLI-based AI coding agent — a command-line program that embeds a full conversational loop with Anthropic's Claude API, a permission-controlled tool execution engine, and a React terminal UI, all bundled into a single binary that runs on your local machine.
The user launches it in a project directory, types natural-language requests, and Claude Code autonomously reads files, edits them, runs shell commands, searches the web, spawns sub-agents, and reports results — all while asking for permission at any step that modifies the environment.
The design is deliberately monolithic: the UI, the agentic loop, the tool system, and the configuration layer all live in one repository and one binary. This is not accidental. A single process means shared state, zero network hops between components, and the ability to render interactive permission prompts in the same terminal where code is being written.
Technology Stack
Understanding the stack before reading the code prevents several common surprises.
Runtime: Bun, not Node.js. Claude Code is built and run with Bun. This choice provides a fast startup, a built-in bundler, and a key feature used pervasively in this codebase: bun:bundle compile-time dead-code elimination (DCE). The feature() call you will see everywhere is not a runtime function — it is a build-time macro that Bun evaluates and removes unreachable branches from the final bundle.
Language: TypeScript 5.x in strict mode. All 1,884 source files are .ts or .tsx. The type system is used aggressively: tool inputs are validated with Zod schemas, the message bus is a discriminated union, and the dependency injection object carries 40+ typed fields.
UI framework: a custom Ink fork. The src/ink/ directory contains a full React reconciler for terminal output, not the npm ink package. It renders React component trees as ANSI escape sequences, using Yoga (compiled to WebAssembly) for CSS Flexbox layout. The rest of the UI is written in React 19 — including a React Compiler pass on some components (look for _c() cache-slot calls in compiled .tsx files).
Schema validation: Zod v4. Tool input schemas are z.object(...) definitions that serve three purposes simultaneously: runtime validation of model-supplied JSON, TypeScript type inference via z.infer<>, and JSON Schema generation for the API's tools parameter.
CLI argument parsing: Commander.js (@commander-js/extra-typings). The src/main.tsx god-function registers dozens of subcommands and options through Commander.
Feature flags: two-layer system. Compile-time: feature('FLAG_NAME') calls in src/tools.ts, src/query.ts, and elsewhere are evaluated by Bun at bundle time; false branches become dead code and are stripped. Runtime: GrowthBook provides remote overrides and A/B experiment allocation, accessed via src/services/analytics/.
The full stack at a glance:
| Technology | Role |
|---|---|
| Bun | Runtime, bundler, compile-time DCE |
| TypeScript 5.x | Language, strict mode throughout |
| React 19 + React Compiler | Terminal UI component tree |
Custom Ink fork (src/ink/) | React reconciler for ANSI terminals |
| Yoga WASM | CSS Flexbox layout for terminal |
@anthropic-ai/sdk | Streaming Claude API client |
@modelcontextprotocol/sdk | MCP server/client protocol |
| Zod v4 | Runtime validation + type inference |
| Commander.js | CLI argument parsing |
| GrowthBook | Runtime feature flags and A/B tests |
| OpenTelemetry | Distributed tracing and metrics |
| lodash-es | Utility functions (memoize, mergeWith, etc.) |
Directory Structure
The src/ directory contains 35 subdirectories and about 18 root-level files. The table below assigns each module a one-line responsibility. Modules marked Core are essential reading; Supporting modules provide important infrastructure; Peripheral modules are feature-specific or auxiliary.
Core Modules
| Module | Files | Role |
|---|---|---|
src/Tool.ts | 1 (793 lines) | The Tool<Input,Output> interface contract and ToolUseContext dependency injection object |
src/query.ts + src/query/ | 5 | The inner agentic loop: API calls, streaming, tool dispatch, context compaction |
src/QueryEngine.ts | 1 (1,296 lines) | Headless conversation engine used by the SDK and non-interactive modes |
src/bootstrap/ | 1 (1,759 lines) | Global singleton state: session ID, cost tracking, model config, telemetry, OAuth |
src/tools/ | 184 | Every tool implementation: BashTool, AgentTool, FileEditTool, FileReadTool, GrepTool, and 20+ more |
src/commands/ + src/commands.ts | 208 | 70+ slash-command implementations and the command registry |
src/screens/ | 3 | The interactive REPL session screen (REPL.tsx, ~3,000 lines) |
src/ink/ | 96 | Custom React reconciler for terminal rendering, Yoga layout, ANSI output |
src/components/ | 389 | All UI components: message display, permission dialogs, prompt input, design system |
src/hooks/ | 104 | React hooks bridging UI events to business logic: permissions, commands, typeahead |
src/state/ | 6 | AppState (150+ fields), minimal pub/sub store, React context provider |
src/services/ | 130 | API client, MCP connections, context compaction, analytics, LSP, OAuth |
src/utils/ | 564 | Largest module: bash security, permissions, settings, model selection, telemetry, and more |
src/entrypoints/ | 8 | cli.tsx bootstrap, init.ts initialization, mcp.ts server mode, SDK type exports |
Supporting Modules
| Module | Files | Role |
|---|---|---|
src/tasks/ | 12 | Background task runners: shell, agent, teammate, workflow |
src/skills/ | 20 | Markdown-driven skill system loaded from .claude/skills/ |
src/bridge/ | 31 | Remote control bridge: mobile and web clients connecting to a local CLI session |
src/cli/ | 19 | Structured output, SSE and WebSocket transports for the bridge |
src/memdir/ | 8 | .claude/memory/ file management for persistent session memory |
src/keybindings/ | 14 | Customizable keyboard shortcut definitions and handlers |
src/constants/ | 21 | API rate limits, beta feature headers, product strings, system prompt templates |
src/context/ | 9 | React contexts for notifications, modal state, mailbox, voice |
Peripheral Modules
| Module | Files | Role |
|---|---|---|
src/coordinator/ | 1 | Coordinator mode for managing networks of worker agents |
src/schemas/ | 1 | Zod schema for the hooks configuration format |
src/migrations/ | 11 | One-time data migrations for the settings file format |
src/vim/ | 5 | Vim key-binding mode for the prompt input field |
src/remote/ | 4 | Remote session management for --remote mode |
src/server/ | 3 | Unix domain socket server for Direct Connect |
src/plugins/ | 2 | Built-in plugin registration |
src/buddy/ | 6 | Companion mascot feature (feature-flagged) |
src/voice/ | 1 | Voice mode feature-flag check |
src/native-ts/ | 4 | TypeScript ports of native libraries (yoga-layout, color-diff) |
src/upstreamproxy/ | 2 | HTTP proxy support for enterprise firewall configurations |
Important Root-Level Files
Several single files at the root of src/ are architecturally central:
| File | Role |
|---|---|
src/main.tsx | Parses all CLI arguments, assembles ToolUseContext, launches REPL or headless mode |
src/tools.ts | Tool registry: imports all tools, applies feature-flag conditional loading |
src/replLauncher.tsx | Bridges main.tsx to the React render root |
src/context.ts | CLAUDE.md file discovery and system context injection |
src/history.ts | Session history read and write |
src/cost-tracker.ts | Per-session API cost tracking |
Architecture: Event-Driven AsyncGenerator Pipeline
Claude Code is not an MVC application. The architecture is an event-driven async generator pipeline — a chain of AsyncGenerator functions that produce stream events and consume tool results in a loop until the model signals it is done.
The following Mermaid diagram shows the high-level structure:
The key insight is that query() in src/query.ts is an AsyncGenerator<StreamEvent>. It does not return a final answer. It yields a stream of typed events — text_delta, tool_use, tool_result, request_start, compact_start, and so on. The REPL screen subscribes to this generator and renders each event incrementally. This is why the terminal updates character by character as Claude types, and why permission prompts can appear mid-stream before the model's turn is finished.
Data Flow: User Input to Rendered Output
Walking through the complete lifecycle of a single user message gives a concrete map of the codebase.
Step 1: CLI entry and fast-path dispatch
src/entrypoints/cli.tsx is the binary's entry point. Its first job is minimizing startup time by avoiding any module loading for common fast paths.
// src/entrypoints/cli.tsx:33-42
async function main(): Promise<void> {
const args = process.argv.slice(2);
// Fast-path for --version/-v: zero module loading needed
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
// MACRO.VERSION is inlined at build time
console.log(`${MACRO.VERSION} (Claude Code)`);
return;
}
// ...
}MACRO.VERSION is a build-time constant inlined by Bun — printing the version requires loading exactly one file. For any other invocation, cli.tsx checks feature-flagged fast paths (bridge mode, daemon workers, Chrome extension MCP) and then dynamically imports main.tsx only after all fast paths have been exhausted. The await import('../main.js') at the end of the function is deliberate: it defers the cost of loading React, Commander, and hundreds of modules until they are actually needed.
Step 2: Initialization and argument parsing
src/entrypoints/init.ts runs the two-phase initialization sequence: environment variable loading, configuration system activation, telemetry setup, LSP initialization, and the trust dialog if running for the first time. It is memoized — the second call returns immediately.
src/main.tsx is the largest file in the codebase at over 4,000 lines. It registers all Commander.js subcommands and options, then determines the run mode (interactive REPL, headless --print, SDK, MCP server, or remote). For the common interactive case, main.tsx assembles the ToolUseContext object and calls launchRepl().
Step 3: The ToolUseContext — the system's dependency injection spine
ToolUseContext, defined in src/Tool.ts:158-300, is the single object that flows through every tool call in the system. It is not a service locator and not a class instance. It is a plain TypeScript object containing everything a tool might need to access: the current message list, the abort controller, the app state getters and setters, permission callbacks, notification hooks, MCP connections, and the configuration options.
// src/Tool.ts:158-179
export type ToolUseContext = {
options: {
commands: Command[]
debug: boolean
mainLoopModel: string
tools: Tools
verbose: boolean
thinkingConfig: ThinkingConfig
mcpClients: MCPServerConnection[]
mcpResources: Record<string, ServerResource[]>
isNonInteractiveSession: boolean
agentDefinitions: AgentDefinitionsResult
maxBudgetUsd?: number
customSystemPrompt?: string
appendSystemPrompt?: string
querySource?: QuerySource
refreshTools?: () => Tools
}
abortController: AbortController
readFileState: FileStateCache
getAppState(): AppState
setAppState(f: (prev: AppState) => AppState): void
// ... 40+ additional fields
}The options sub-object contains session-wide static configuration. The remaining fields are callbacks and state accessors that allow tools to read and update the running UI without coupling to any specific rendering framework. When a sub-agent is spawned by AgentTool, it receives a cloned ToolUseContext with a no-op setAppState — this is how the sub-agent's state changes are isolated from the parent session.
Step 4: REPL rendering and message submission
launchRepl() in src/replLauncher.tsx dynamically imports App and REPL, then hands them to the Ink renderer:
// src/replLauncher.tsx:12-22
export async function launchRepl(
root: Root,
appProps: AppWrapperProps,
replProps: REPLProps,
renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>
): Promise<void> {
const { App } = await import('./components/App.js');
const { REPL } = await import('./screens/REPL.js');
await renderAndRun(root, <App {...appProps}><REPL {...replProps} /></App>);
}The REPL component (src/screens/REPL.tsx) owns the interactive session. It renders the message history, the prompt input, and active permission dialogs. When the user presses Enter, REPL.tsx calls QueryEngine.submitMessage(), which drives the inner query loop.
Step 5: The query loop
src/query.ts contains the inner agentic loop. It makes the streaming API call, iterates over StreamEvent values yielded by the API client, dispatches tool_use blocks to the tool orchestrator, appends tool_result messages back to the conversation, and loops until stop_reason === 'end_turn' or the abort controller fires.
The loop also monitors token usage. When the context window approaches its limit, it triggers the compaction service in src/services/compact/autoCompact.ts, which summarizes the oldest messages and splices the summary into the conversation before continuing.
Step 6: Tool execution
src/services/tools/ contains the StreamingToolExecutor. When the query loop encounters a tool_use block, the executor:
- Looks up the tool by name in the tool registry.
- Calls
canUseTool()(src/hooks/useCanUseTool.tsx) to run the permission check. In interactive mode this may render a permission prompt to the terminal and block until the user responds. In headless mode it consults the configured permission rules automatically. - Calls
tool.call(args, context, canUseTool, parentMessage, onProgress)with the validated input. - Receives a
ToolResult<Output>containing the data and optional new messages to inject. - Serializes the result as a
UserMessagewithtype: 'tool_result'blocks, appends it to the conversation, and the query loop continues.
Step 7: Sub-agent recursion
src/tools/AgentTool/runAgent.ts implements the recursive sub-agent path. When the model calls AgentTool, the tool clones the parent ToolUseContext (forking the message list and replacing setAppState with a no-op), then starts its own independent query() loop. The sub-agent can use tools, make API calls, and produce results — all contained within the parent turn. The parent resumes after the sub-agent's final result is returned as a ToolResult.
Step 8: Context injection — CLAUDE.md
Before each API call, src/context.ts scans from the current working directory up to the home directory, reading every CLAUDE.md file found. These files contain project-specific instructions that are injected into the system prompt. The scan result is memoized to avoid repeated filesystem access within a session. This is how a user can place a CLAUDE.md at the project root with project-specific rules (code style, test commands, context about the codebase) that Claude will follow for the entire session.
The Tool Interface
Every tool in src/tools/ satisfies the Tool<Input, Output, Progress> interface from src/Tool.ts:362-466. Understanding this interface is prerequisite knowledge for reading any tool implementation.
// src/Tool.ts:362-466
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
aliases?: string[]
searchHint?: string
call(
args: z.infer<Input>,
context: ToolUseContext,
canUseTool: CanUseToolFn,
parentMessage: AssistantMessage,
onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>>
description(
input: z.infer<Input>,
options: {
isNonInteractiveSession: boolean
toolPermissionContext: ToolPermissionContext
tools: Tools
},
): Promise<string>
readonly inputSchema: Input // Zod schema — used for validation and JSON Schema generation
readonly inputJSONSchema?: ToolInputJSONSchema // For MCP tools that supply JSON Schema directly
outputSchema?: z.ZodType<unknown>
isConcurrencySafe(input: z.infer<Input>): boolean // Can this run in parallel with other tools?
isEnabled(): boolean
isReadOnly(input: z.infer<Input>): boolean
isDestructive?(input: z.infer<Input>): boolean // Irreversible operations (delete, overwrite)
interruptBehavior?(): 'cancel' | 'block'
readonly shouldDefer?: boolean // Require ToolSearch before calling this tool
readonly alwaysLoad?: boolean // Never defer; always include in initial prompt
mcpInfo?: { serverName: string; toolName: string }
readonly name: string
maxResultSizeChars: number // Overflow → result saved to disk, Claude gets file path
}A few fields deserve attention:
isConcurrencySafe governs whether the tool executor will run this tool in parallel with others. FileReadTool returns true; BashTool returns false unless the command is a known read-only operation.
interruptBehavior controls what happens when the user submits a new message while the tool is running. 'cancel' means abort immediately; 'block' means keep running and queue the new message. Most long-running tools use 'block'.
maxResultSizeChars is a budget cap. If the tool produces more output than this limit, the full result is written to a temporary file and Claude receives a truncated preview with the file path. This prevents large BashTool outputs from filling the context window.
shouldDefer and alwaysLoad work with the ToolSearch mechanism (see Chapter 9). When shouldDefer is true, the tool's full schema is omitted from the initial API call and the model must explicitly search for it before it can be used. This keeps the initial prompt short when the tool set is large.
The Tool Registry and Feature Flags
src/tools.ts assembles the complete list of tools available in a session. It illustrates the two-layer feature flag system concretely:
// src/tools.ts:14-53 (abridged)
// Always-available tools (unconditional imports at the top of the file)
import { AgentTool } from './tools/AgentTool/AgentTool.js'
import { BashTool } from './tools/BashTool/BashTool.js'
import { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
import { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
// Ant-internal-only tools: runtime env check, require() call
const REPLTool =
process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
: null
// Feature-flagged tools: compile-time DCE via bun:bundle
const SleepTool =
feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null
const cronTools = feature('AGENT_TRIGGERS')
? [
require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
]
: []When Bun builds the external release bundle, feature('PROACTIVE') evaluates to false at build time. The require('./tools/SleepTool/...') branch becomes unreachable and is removed from the bundle entirely — the SleepTool module does not exist in the external binary. This is not an if statement that runs at startup; it is a build-time directive.
The process.env.USER_TYPE === 'ant' checks operate at runtime and survive the build. They gate tools that are deployed in production but restricted to internal Anthropic use.
Key Configuration
CLAUDE.md
CLAUDE.md files are project-specific instruction documents that Claude Code automatically reads and injects into every API call's system prompt. The discovery algorithm (src/context.ts) walks up the directory tree from the current working directory to the home directory, reading each CLAUDE.md it finds. Files closer to the project root take precedence, and all found files are concatenated.
A typical CLAUDE.md at the project root might specify:
- Build and test commands for the project
- Code style conventions Claude should follow
- Important context about the codebase that would otherwise require exploration
This is the primary extension point for per-project customization without any code changes.
settings.json
Settings are loaded from multiple sources and merged in priority order. The merge happens in src/utils/settings/:
- User-level settings:
~/.claude/settings.json - Project-level settings:
.claude/settings.jsonin the project root - Enterprise MDM/registry settings (Windows HKCU, macOS MDM profile)
- CLI flags passed at invocation time
- Remote hosted settings (when running in Claude Code Remote)
Higher-priority sources win on conflict. The result is an immutable Settings object for the session.
Settings control allowed tools, bash command allowlists and blocklists, hook configurations, permission mode, and dozens of behavioral flags.
Feature Flags
The two-layer flag system has been mentioned, but it is worth summarizing the consumer interface:
// Compile-time (bun:bundle macro — branch is DCE'd in production builds)
if (feature('SOME_INTERNAL_FLAG')) {
// This entire block does not exist in the external binary
}
// Runtime (GrowthBook — evaluated during the session)
import { isFeatureEnabled } from './services/analytics/growthbook.js'
if (await isFeatureEnabled('some-runtime-flag')) {
// Evaluated against the remote GrowthBook instance
}As a reader of the source code, you will encounter feature() calls constantly. When you see one, the practical meaning is: "this block only exists in Anthropic-internal builds." If you are reading to understand the external product's behavior, treat the feature() branch as removed.
The Bootstrap Singleton
src/bootstrap/state.ts is the global state container for the process. It is explicitly not a React state store — it predates the React render cycle and outlives any individual session.
// src/bootstrap/state.ts:31
// DO NOT ADD MORE STATE HERE - BE JUDICIOUS WITH GLOBAL STATE
type State = {
originalCwd: string
projectRoot: string
totalCostUSD: number
totalAPIDuration: number
cwd: string
modelUsage: { [modelName: string]: ModelUsage }
isInteractive: boolean
// ... 80+ additional fields
}The comment is load-bearing. This file has grown to 1,759 lines and ~80 getter/setter functions. Adding state here requires justification because it makes parallel sub-agent scenarios harder to reason about. The AppState in src/state/ is the correct place for per-session React state; bootstrap/state.ts is for process-level invariants that must survive across sessions or be accessible before the React tree is mounted.
Architecture Summary Diagram
The following diagram places every major module in its layer:
Key Takeaways
Claude Code is a monolithic single-binary CLI that combines an AI agentic loop, a full terminal UI, and a tool execution engine in one TypeScript/Bun application.
The core abstraction is the Tool<Input, Output> interface. Every action Claude can take is expressed as a tool that receives a ToolUseContext, performs its work, and returns a ToolResult. The context object is the system's dependency injection spine — it carries everything from abort signals to permission callbacks without global variable access.
The query loop in src/query.ts is an AsyncGenerator that yields stream events and loops until the model signals completion. This generator-based design enables incremental rendering, mid-stream permission prompts, and context compaction without restructuring the control flow.
Feature flags (feature('FLAG')) are compile-time macros that eliminate entire code branches in external builds. When reading the source, treat any feature() block as internal-only and skip it when reasoning about external product behavior.
Configuration has three layers: CLAUDE.md files for per-project instructions injected into every system prompt, settings.json for behavioral configuration merged from multiple sources, and feature flags for capability gating.
The 35 modules follow a clear separation of concerns: src/entrypoints/ for startup, src/query.ts for the loop, src/tools/ for actions, src/components/ for rendering, src/services/ for external integrations, and src/utils/ for shared infrastructure. If you can locate a concern in this map, you can find its code.
Next: Chapter 02 traces the complete startup sequence from cli.tsx through init.ts to the first rendered REPL prompt — following every dynamic import and initialization step in order.