Claude Code Source Analysis (Part 1): Core Modules and Data Flow

Using AI to read AI's source code — that's kind of meta, isn't it?

Posted by laosuan on April 1, 2026
中文 English

Why Read Claude Code’s Source?

Most people who use Claude Code stay at the “I can use it” level.

That’s fine. But between “using it” and “understanding it” lies a massive gap. On this side of the gap, it’s a black box — you type something, it spits out code, and you have no idea what happened in between.

Cross that gap, and you’ll discover: Claude Code’s architecture is itself an excellent software engineering lesson.

This is the first post in a series. I’ll walk you from the entry file, tracing through to API calls and tool execution, covering the entire data flow end to end.

After reading this, when you dive into the source code yourself, you’ll be much more efficient — because you’ll already have the map.

Where to Start

When facing an unfamiliar codebase, most people’s first reaction is “where do I start?”

There’s actually a very simple answer: Start with the entry files and configuration files.

Entry files tell you how the program boots. Config files tell you what it depends on. Once you understand these two, you have the program’s “skeleton.” Everything else is just flesh on the bones.

Claude Code’s entry file is src/entrypoints/cli.tsx.

Layer 1: cli.tsx — Where Everything Begins

Open cli.tsx, and the first thing you see is a bunch of “polyfills” — before the program actually runs, it injects some things into the global environment:

// Top of cli.tsx
const feature = (_name: string) => false;  // All feature flags return false
globalThis.MACRO = {
    VERSION: "1.0.x",
    // ... build-time constants
};

That feature = () => false line is crucial. Claude Code has tons of feature flags internally: COORDINATOR_MODE (multi-agent coordination), DAEMON (background daemon), VOICE_MODE (voice mode)… these are all Anthropic-internal features. In the open-source version, a single () => false shuts them all off.

This is a very practical engineering technique — using feature flags to control feature toggles. When shipping, you only need to change one line of code to strip out all internal features.

The actual startup logic is at the bottom of the file:

// cli.tsx:319
void main();

// cli.tsx:59
async function main(): Promise<void> {
    const args = process.argv.slice(2);

    // Fast path: --version outputs directly, zero module loading
    if (args[0] === "--version") {
        console.log(`${MACRO.VERSION} (Claude Code)`);
        return;
    }

    // ...other fast paths (mostly skipped by feature flags)

    // Key transition: dynamic import of main.tsx
    const { main: cliMain } = await import("../main.jsx");  // L312
    await cliMain();                                          // L314
}

Notice this uses dynamic import(), not static import. This means main.tsx and all its dependencies are only loaded when you actually need to start the full CLI. If you just run --version, the entire program exits with virtually zero overhead.

This is “fast path” thinking in software engineering: handle common simple cases as quickly as possible. Don’t make them go through the full complex flow.

Layer 2: main.tsx — Commander Parsing and Initialization

main.tsx is about 1,700 lines — the CLI’s definition center. It uses Commander.js to parse command-line arguments, then runs the initialization flow.

Core call chain:

main.tsx:585    export async function main()
  ↓
main.tsx:4504   program.parseAsync(process.argv)   // Commander parsing
  ↓
main.tsx:1006   .action(async (prompt, options) => { ... })  // Main command handler
  ↓
main.tsx:916    await init()    // Initialization
  ↓
main.tsx:3798   await launchRepl(...)   // Launch interactive REPL

init() is defined in src/entrypoints/init.ts:57 and is memoized (runs only once). It does a lot, but each step has a clear purpose:

export const init = memoize(async (): Promise<void> => {
    enableConfigs();                        // Load config system
    applySafeConfigEnvironmentVariables();  // Safe env vars
    applyExtraCACertsFromConfig();          // TLS certificates
    setupGracefulShutdown();               // Graceful shutdown
    populateOAuthAccountInfoIfNeeded();    // OAuth info
    configureGlobalMTLS();                 // mTLS config
    configureGlobalAgents();               // Proxy config
    preconnectAnthropicApi();              // TCP pre-connect to API
});

That last line, preconnectAnthropicApi(), is interesting — before you’ve typed anything, the program is already establishing a TCP connection with the API server. This “pre-connect” strategy noticeably reduces the wait time for your first query.

After initialization, launchRepl() starts the interactive interface. It’s defined in src/replLauncher.tsx:12:

export async function launchRepl(root, appProps, replProps, renderAndRun) {
    const { App } = await import('./components/App.js');
    const { REPL } = await import('./screens/REPL.js');
    await renderAndRun(root, <App {...appProps}>
        <REPL {...replProps} />
    </App>);
}

Yes, the terminal UI is built with React. It uses a library called Ink that brings React’s component model to the terminal. Every input box, every message, every loading animation you see in the terminal — they’re all React components.

Layer 3: REPL.tsx — The Entry Point for User Input

REPL.tsx is the core interactive component. When you type something in the terminal and press Enter, it triggers the onQuery callback here.

Key call chain:

User presses Enter
  ↓
REPL.tsx:2859   onQuery(newMessages, ...)
  ↓
REPL.tsx:2922   → onQueryImpl(...)
  ↓
REPL.tsx:2772   → await Promise.all([       // Load context in parallel
                      getSystemPrompt(...),   // System prompt
                      getUserContext(),        // CLAUDE.md user instructions
                      getSystemContext()       // git status, environment info
                  ])
  ↓
REPL.tsx:2797   → for await (event of query({...}))  // Streaming query
  ↓
REPL.tsx:2806       onQueryEvent(event)   // Handle each streaming event

Two design choices worth noting here:

First, context loading is parallelized. Promise.all fetches the system prompt, user context (your project’s CLAUDE.md file), and system context (git status, current date, etc.) simultaneously. Sequential loading would work too, but parallel is faster.

Second, the query is streamed. for await...of consumes an AsyncGenerator, processing each event the instant it arrives. This is why you see Claude’s response appearing character by character — it’s not waiting for the entire response to generate before displaying. It streams as it generates.

One easily overlooked detail: REPL.tsx calls query() directly, not through QueryEngine. QueryEngine (defined in QueryEngine.ts:186) is the orchestrator for SDK/headless mode. Both paths ultimately converge into the same query() function.

Layer 4: query.ts — The Heart of the Conversation Loop

query.ts is the core of the entire system — a while(true) loop.

// query.ts:219
export async function* query(params: QueryParams) {
    yield* queryLoop(params, consumedCommandUuids);
}

// query.ts:241
async function* queryLoop(params, consumedCommandUuids) {
    const deps = params.deps ?? productionDeps();

    // Main loop: each iteration = one API call + tool execution
    while (true) {
        // 1. Call the model
        for await (const message of deps.callModel({
            messages, systemPrompt, tools, signal, ...
        })) {
            // Handle streaming events: text, thinking, tool_use...
        }

        // 2. Execute tool calls
        const toolUpdates = runTools(toolUseBlocks, ...);
        for await (const update of toolUpdates) {
            yield update.message;
        }

        // 3. Decision: tool_use → continue, end_turn → return
    }
}

The loop’s logic in one sentence: Call the API → get a response → if the response contains tool calls, execute the tools, stuff the results back in, and call the API again → until the model says “I’m done” (end_turn).

This is the so-called Agentic Loop — the core pattern of an Agent. Claude doesn’t just answer your question; it actively completes tasks by calling tools (reading files, writing files, executing commands). Each tool call is one iteration of the loop.

deps.callModel is a dependency injection point. In production, it points to queryModelWithStreaming (from src/services/api/claude.ts):

// query/deps.ts:33
export function productionDeps(): QueryDeps {
    return {
        callModel: queryModelWithStreaming,  // → claude.ts
        microcompact: microcompactMessages,
        autocompact: autoCompactIfNeeded,
        uuid: randomUUID,
    };
}

The benefit of dependency injection here: during testing, you can swap in a mock API call without actually hitting the Claude API. This is standard practice for writing testable code.

Layer 5: claude.ts — Where the Request Actually Goes Out

services/api/claude.ts is the bottom layer, directly calling the Anthropic SDK.

// claude.ts:753
export async function* queryModelWithStreaming({ messages, systemPrompt, ... }) {
    yield* queryModel(messages, systemPrompt, ...);
}

// claude.ts:1018
async function* queryModel(messages, systemPrompt, ...) {
    // ... build request params, compute beta headers, filter tools ...

    // The actual API call
    const result = await anthropic.beta.messages
        .create(
            { ...params, stream: true },
            { signal, headers }
        )
        .withResponse();

    // Iterate over streaming events
    for await (const event of stream) {
        // message_start, content_block_delta, tool_use, message_stop...
        // Build AssistantMessage and yield back to query.ts
    }
}

That stream: true is the key to streaming. It makes the API push tokens back one at a time instead of waiting until all content is generated.

Claude Code also supports multiple providers: Anthropic Direct, AWS Bedrock, Google Vertex, and Azure. You can switch backends through configuration.

The Complete Data Flow: Full Picture of a Single Interaction

Connecting all five layers, here’s what a complete interaction looks like:

User types in terminal: "Write me a sorting function"
  │
  │  cli.tsx:319     void main()
  │  cli.tsx:312     Dynamic import main.tsx
  │  cli.tsx:314     cliMain()
  ▼
  main.tsx:585       main() starts
  main.tsx:916       init() → load config, auth, pre-connect API
  main.tsx:3798      launchRepl() → render React/Ink component tree
  │
  ▼  User input → captured by React component
  REPL.tsx:2859      onQuery()
  REPL.tsx:2772      Parallel load: system prompt + CLAUDE.md + git status
  REPL.tsx:2797      for await (event of query({...}))
  │
  ▼
  query.ts:307       while (true) {  ← Main loop begins
  query.ts:659         Call API (streaming)
  │                      ↓
  claude.ts:1823         anthropic.beta.messages.create({stream: true})
  claude.ts:1880+        Iterate streaming events → yield back to query.ts
  │                    ↑
  query.ts:1385        If tool_use → execute tools (read files, write files, etc.)
  query.ts              Append tool results to messages → continue (call API again)
  query.ts              If end_turn → return (exit loop)
  │
  ▼
  REPL.tsx:2806      onQueryEvent(event) → update terminal UI
  REPL.tsx:2851      resetLoadingState() → restore input state

Tech Stack: Every Choice Has a Reason

Technology Why
Bun Faster than Node.js, native TSX/ESM support, extremely fast startup
React + Ink React’s component model for terminal UI — reuses frontend devs’ mental model
React Compiler Automatic memoization, reduces unnecessary terminal re-renders
Commander.js Mature CLI argument parsing framework
Anthropic SDK Official SDK with native streaming support
Zustand-style Store Lightweight state management, ideal for single-process CLI
MCP Protocol Anthropic’s tool extension protocol for plugging in external tool servers

The most interesting choice is React + Ink. Using React for terminal UI sounds weird, but think about it — terminal UI is essentially a component tree too, requiring state management and reactive updates. React’s declarative programming model applies perfectly here.

Claude Code even forked the Ink framework, building their own reconciler and virtual list implementation. This shows they have high performance requirements for terminal rendering.

The Tool System: The Agent’s “Hands”

Claude Code’s core capability comes from the tool system. Each tool is a function:

interface Tool {
    name: string
    description: string
    inputSchema: JSONSchema    // Parameter definitions
    call(input, context): Result  // Execution logic
}

Core tools include:

  • BashTool — execute shell commands
  • FileReadTool / FileEditTool / FileWriteTool — file operations
  • GlobTool / GrepTool — search
  • AgentTool — spawn sub-agents (Agents can spawn Agents!)
  • WebFetchTool / WebSearchTool — network operations

When Claude’s response contains tool_use, the main loop in query.ts executes the corresponding tool, appends the result as a tool_result message to conversation history, then calls the API again. This “call-execute-feedback” loop is the secret behind how Agents can autonomously complete complex tasks.

A Methodology for Reading Source Code

Finally, let me share my approach to reading this codebase.

Step 1: Read the entry files and config files. This gives you the program’s skeleton — where it starts, what it depends on, how it builds.

Step 2: Trace the main data flow. Find where user input enters the system, then trace it through every module until the final output. Don’t get lost in details early on. Walk the main path first.

Step 3: Dive into key modules. Once you’ve traced the main path, pick the modules you’re interested in and go deep. The tool system, permission system, auto-compaction mechanism, etc.

This is the same approach as reading a book: scan the table of contents first, then read through once, then study the key chapters closely. Many people read source code by grinding line by line from the start. After half a day, they still don’t know what they’re looking at. That’s because they never built the big picture first.

Claude Code’s codebase has about 1,341 TypeScript errors (because it’s decompiled), but that doesn’t affect Bun execution at all. This is also an interesting engineering reality: The type system is a tool, not the goal. Running is what matters.


Next up, I’ll deep-dive into the tool system and permission mechanism — how Claude Code decides whether it should execute a dangerous operation.

Stay tuned.

阅读本文的中文版本