Inside Claude Code: 1,900‑File Source Dive Reveals Six‑Layer Architecture
After a source‑map leak exposed Claude Code’s 1,900 TypeScript files, this analysis dissects its six‑layer architecture, dynamic prompt assembly, four‑level caching, 60+ tool governance pipeline, six built‑in agents, five context‑compression strategies, and the real engineering trade‑offs hidden beneath the product.
Leak Incident
On 2026‑03‑31 the npm package @anthropic-ai/claude-code (v2.1.88) shipped a 59.7 MB source‑map that exposed the full TypeScript codebase (≈1,900 files, >512 k lines). Within hours the repository was forked >41,500 times and covered by major tech outlets.
Six‑Layer Architecture
The source tree under src/ is organized into six logical layers:
UI Layer : screens/, components/, ink/ Entry & Bootstrap Layer : entrypoints/, main.tsx, bootstrap/ Application Core : REPL handling, command dispatch, query engine ( QueryEngine.ts, query.ts, commands.ts, state/)
Tool & Agent Layer : tools/, tasks/, coordinator/ Service Layer : API, MCP protocol, auth, compression, telemetry ( services/)
Extension Layer : IDE bridge, remote sessions, skills, voice, memory ( bridge/, remote/, skills/, voice/, memdir/)
The stack runs on strict‑mode TypeScript + Bun, React 18, a custom Ink renderer, Yoga Flexbox, Zod validation, and Commander.js for CLI parsing.
Prompt Assembly Machine
System prompts are built dynamically by getSystemPrompt() and split at SYSTEM_PROMPT_DYNAMIC_BOUNDARY. The static portion (global scope) contains role definitions, safety rules, and coding conventions. The dynamic portion is recomputed per session and can inject project‑specific guidance, tool preferences, and live Git status.
Static sections (registered with scope: 'global') include: getSimpleIntroSection() – role definition and safety directives getSimpleSystemSection() – markdown rendering, tool‑permission mode, prompt‑injection protection getSimpleDoingTasksSection() – core coding behavior policy (e.g., never add unrequested features, avoid premature abstraction, read before edit, report results honestly) getActionsSection() – classification of destructive actions and confirmation workflow getUsingYourToolsSection() – prefer specialized tools over generic Bash commands
Dynamic sections are registered via systemPromptSection() and cached per session; they can be cleared with /clear or compacted with /compact. MCP‑driven sections are always uncached because the server may connect or disconnect between turns.
Four‑Level Cache System
Claude Code caches prompts at four scopes to minimise token usage:
Global – shared across organizations; static system prompt.
Org – organization‑wide prefix and base prompt.
Session – memoised section results for the current session.
Turn – per‑turn message cache control.
Tool schemas are also memoised by tool.name to avoid re‑parsing when feature flags flip.
Tool System – 60+ Tools and Governance Pipeline
Each tool implements the generic interface Tool<Input, Output, Progress> (defined in Tool.ts, 793 lines). The interface defines: call() – core execution prompt() – usage description sent to the API checkPermissions() – tool‑specific permission logic isConcurrencySafe(), isReadOnly(), isDestructive() – capability flags mapToolResultToToolResultBlockParam() – converts raw output to API format renderToolUseMessage() – UI/Ink rendering
Tools are registered via getAllBaseTools() and fall into three loading categories:
Always loaded (core tools such as BashTool, FileReadTool, AgentTool).
Conditionally loaded based on feature flags or user type (e.g., internal ant tools).
Feature‑flag gated with compile‑time dead‑code elimination using feature() and Bun bundling.
When the total number of tools exceeds a threshold, a “deferred loading” flag ( defer_loading: true) hides their schemas from the model; the model must request them via ToolSearchTool, keeping the system prompt under ~200 K tokens.
The 14‑step execution pipeline is:
Locate tool (or alias) in the registry.
Validate input with Zod schema.
Run custom tool.validateInput().
Back‑fill observable fields.
Run PreToolUse hooks (may modify input, add messages, or abort).
Resolve hook‑based permission decisions.
Execute final permission check via canUseTool().
If Bash, start a speculative classifier in parallel.
Execute tool.call().
Record analytics, tracing, OpenTelemetry.
Run PostToolUse hooks (may modify MCP output).
Map result to API block.
If result exceeds maxResultSizeChars, persist to disk.
Yield MessageUpdate to the UI.
Concurrency is managed by StreamingToolExecutor. Tools that report isConcurrencySafe() may run in parallel; non‑concurrent tools (e.g., FileEditTool, AgentTool) acquire exclusive execution. A Bash failure aborts sibling processes via siblingAbortController.abort('sibling_error').
Permission System – Five‑Tier Security Model
Implemented in useCanUseTool.tsx (~40 K lines), the system offers five public modes: default – rule‑based match, fallback to confirmation dialog. plan – read‑only tools auto‑allow; write tools require confirmation. acceptEdits – automatically approve file edits. bypassPermissions – opt‑in full allow. dontAsk – automatically reject unsafe commands.
Decision flow:
alwaysAllow → alwaysDeny → alwaysAsk → Bash classifier → transcript classifier → user dialog. Rules support glob patterns and detect overly broad permissions.
Agent System – Six Built‑In Agents
Claude Code defines six agents, each with distinct constraints:
General Purpose – full tool set.
Explore – read‑only (Glob, Grep, FileRead; Bash limited to ls / git).
Plan – read‑only, outputs an implementation plan.
Verification – “try to break it” philosophy, runs adversarial probes and reports PASS/FAIL/PARTIAL.
Claude Code Guide – answers product‑specific questions.
Statusline Setup – read + edit only for status bar configuration.
Agent dispatch mirrors the tool pipeline but adds agent‑specific steps such as constructing a dedicated ToolUseContext, registering hooks/skills/MCP servers, and reusing the parent system prompt to save cache bytes for forked agents.
Context Economics – Five Compression Strategies
Claude Code treats context as a scarce resource and provides five compaction mechanisms:
Auto‑Compact – triggered when token budget reaches context_window - 13 000; runs a summarisation model over the whole history.
Micro‑Compact – compresses an oversized tool output.
Snip‑Compact – trims early history while preserving recent assistant messages.
Reactive‑Compact – incremental compression during streaming responses.
Context‑Collapse – folds repetitive structural data gated by feature flags.
Auto‑Compact failures were logged in 1,279 sessions with >50 consecutive failures (up to 3,272), wasting ~250 k API calls daily. The fix is a circuit‑breaker constant MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3.
Attachment System – On‑Demand Context Injection
Before each API call, getAttachmentMessages() injects a <system‑reminder> block containing:
File references via @ syntax.
Memory snippets (up to 5 files, 4 KB each, 20 KB per turn, 60 KB total).
Deferred tool delta, MCP instruction delta, task outputs, IDE selections, hook feedback, LSP diagnostics, skill recommendations.
Strict limits keep the overall token budget manageable.
Ecosystem – Skills, Plugins, and MCP
Three extension mechanisms make the model aware of its capabilities:
Skill : markdown‑based workflow packages with front‑matter declaring allowed-tools. The model must invoke the Skill tool to execute a skill.
Plugin : richer than a CLI plugin; can add markdown commands, SKILL.md directories, metadata, user config, shell front‑matter, and model hints, thereby altering model behaviour.
MCP (Model Context Protocol) : when an MCP server connects, its instructions are merged into the system prompt, effectively adding new tools and usage guidance. Implementation lives in services/mcp/ (≈944 KB) and supports stdio, SSE/HTTP, WebSocket, and SDK transports.
All three ensure the model can see and reason about its current ability list, a problem many AI‑coding platforms ignore.
Technical Debt – Real‑World Engineering Trade‑offs
Despite its sophistication, Claude Code carries classic debt:
God Component : REPL.tsx is a 5,005‑line component with 68 useState, 44 useCallback, 227 hook calls, 22‑level JSX nesting, and 300+ conditionals – effectively untestable in isolation.
Feature‑Flag Overload : 89 distinct compile‑time flags referenced 960 times (e.g., six KAIROS flags) create parallel product paths within the same codebase.
Circular Dependencies : 61 files contain import‑cycle work‑arounds, centred around the massive Tool.ts type definition (≈792 lines) that pulls in permissions, messages, analytics, MCP, and agent types.
Excessive Type Assertions : >1,200 analytics calls use a 53‑character type cast to silence CI checks.
Hidden Features : encoded pet system (“duck”, “goose”) via String.fromCharCode to avoid CI string‑leak detection; KAIROS background agent, Buddy System, and an “Undercover Mode” that strips internal Anthropic identifiers.
Rohan’s final note: “the code ships. it works. lots of developers rely on it daily. that matters more than clean architecture. but it's worth being honest about the cost.”
Product Insights – Design Principles
Never trust model self‑awareness – encode desired behaviours as explicit prompt sections (e.g., getSimpleDoingTasksSection()).
Separate roles – distinct agents for execution and verification prevent confirmation bias.
Govern tool usage – input validation, permission checks, and post‑hooks are essential.
Treat context as a budget – cache aggressively, load lazily, and compress aggressively.
Make extensions visible to the model – Skills, Plugins, and MCP give the model a self‑aware capability list.
Acknowledge debt – large components, flag sprawl, and circular imports are acceptable if the system ships and serves users, but they should be tracked and gradually refactored.
Key Code Snippets
const TOOL_DEFAULTS = {
isConcurrencySafe: () => false, // default unsafe (assume side‑effects)
isReadOnly: () => false, // default write‑capable
checkPermissions: (input) => ({ behavior: 'allow', updatedInput: input })
}; function canExecuteTool(isConcurrencySafe) {
const executingTools = this.tools.filter(t => t.status === 'executing');
return (
executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
);
}Image illustrating the six‑layer stack:
ArcThink
ArcThink makes complex information clearer and turns scattered ideas into valuable insights and understanding.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
