2. Architecture Overview
Component map
┌─────────────────────────────────────────────┐
│ entrypoints/cli.tsx (fast-path dispatcher) │
└──────────────┬──────────────────────────────┘
│
┌──────────────┬───────────┼─────────────┬─────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼
--version claude bridge claude daemon claude ps/bg environment fall-through
(MACRO) bridge/*.ts daemon/*.ts cli/bg.ts -runner → main.tsx
│
▼
┌──────────────────────────────────────────┐
│ main.tsx (~4.7k LOC) │
│ · enable configs, plugins, hooks │
│ · load commands, tools, agents, MCP │
│ · decide: interactive | headless │
└──────────────┬───────────────────────────┘
│
┌─────────────────────────────────┼────────────────────────┐
▼ ▼ ▼
renderAndRun() runHeadless() resume path
(Ink TUI, components/) (QueryEngine directly) (sessionStorage)
│ │
│ user prompt / queue │ programmatic prompt
▼ ▼
┌──────────────────────────────────────────────────┐
│ QueryEngine (QueryEngine.ts) │
│ per-conversation state: messages, usage, │
│ permissions, file cache, discovered skills │
└──────────────┬───────────────────────────────────┘
│ submitMessage() → async generator of SDKMessage
▼
┌──────────────────────────────────────────────────┐
│ query() (query.ts) — the agent loop │
│ while(true): │
│ • call model (stream) │
│ • dispatch tool_use blocks in parallel │
│ • execute tools via Tool.call() │
│ • feed tool_results back │
│ • maybe compact / escalate / fallback │
└──────────────┬───────────────┬──────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────┐ ┌─────────────────┐
│ services/api/ │ │ tools/* │ │ services/compact │
│ claude.ts │ │ 40+ tools│ │ autoCompact │
│ (streaming, │ │ MCP │ │ compact.ts │
│ retry, thinking, │ │ plugins │ │ summarize │
│ caching) │ │ skills │ │ tool state │
└──────────────────┘ └──────────┘ └─────────────────┘
Layered structure
The tree is flat by folder but layered by import direction:
| Layer | Folders | Responsibility |
|---|---|---|
| Leaf / pure | constants/, types/, schemas/ |
Pure data, no side effects. constants/ is explicitly leaf-of-DAG at module-load time (constants/product.ts:60-63). |
| Utilities | utils/ (329 files) |
File I/O, git, bash AST, fileHistory, caches, crypto, terminal, sandbox, analytics, plugin loader, sinks, ... |
| Core services | services/api, services/compact, services/mcp, services/SessionMemory, services/extractMemories, services/policyLimits |
Non-UI capabilities that outlive any one turn. |
| Tool implementations | tools/* |
One folder per tool (Bash, Read, Edit, Agent, Skill, …) + tools.ts registry. |
| Agent loop | query.ts, Tool.ts, QueryEngine.ts, Task.ts, tools.ts, commands.ts |
The heart: composes tools + model + history into a generator. |
| State | state/, bootstrap/state.ts, context/ |
AppState store + React context providers + module-level bootstrap singletons. |
| UI (Ink/React) | components/, hooks/, ink/, screens/, keybindings/, vim/ |
Interactive rendering, keyboard, dialogs. |
| Entry orchestration | entrypoints/, main.tsx, setup.ts, cli/ |
Boot, dispatch, handler wiring. |
| Remote / external | bridge/, remote/, upstreamproxy/, server/, plugins/, native-ts/ |
Remote sessions, proxy, MCP servers shipped alongside. |
Key abstractions
Tool — Tool.ts:362
The canonical extension point. Every capability Claude can take is a Tool<Input, Output> with:
call(input, context, canUseTool, parentMessage, onProgress)— the executor.description(input, options)async — shown in permission dialogs.prompt(getToolPermissionContext, tools, agents)async — the text the model sees.inputSchema(Zod) + optionalinputJSONSchema(for MCP).isReadOnly,isDestructive,isConcurrencySafe,isSearchOrReadCommand,shouldDefer,alwaysLoad— metadata that drives UI and policy.checkPermissions,validateInput,getPath,preparePermissionMatcher— permission hooks.maxResultSizeChars— output-to-disk threshold;Infinityfor self-bounding tools like Read (Tool.ts:466).
Command — types/command.ts:205
Three flavors: 'prompt' (injects text; invoked via SkillTool or AgentTool), 'local' (runs client-side without the model), and 'local-jsx' (renders Ink UI). Slash commands and skills share the same type — a skill IS a command with type: 'prompt'.
AgentDefinition — tools/AgentTool/loadAgentsDir.ts:162
Union of BuiltInAgentDefinition | CustomAgentDefinition | PluginAgentDefinition. Each has agentType, whenToUse, tools allowlist, optional disallowedTools, optional mcpServers, model, effort, and getSystemPrompt(). Loaded via getAgentDefinitionsWithOverrides() (loadAgentsDir.ts:296).
QueryEngine — QueryEngine.ts:184
"One QueryEngine per conversation. Each submitMessage() call starts a new turn within the same conversation."
It owns mutableMessages, abortController, permissionDenials, totalUsage, readFileState, discoveredSkillNames, loadedNestedMemoryPaths. Wraps query() for headless / SDK callers.
ToolUseContext — Tool.ts (referenced widely)
A bag passed into every tool.call() containing: options (model, tools, mcpClients, agentDefinitions, thinkingConfig), AppState getters/setters, abortController, readFileState, permission context callbacks, file-history / attribution updaters, and skill/memory trigger sets.
AppState — state/AppStateStore.ts
A large immutable struct managed via a tiny singleton store (state/store.ts). Covers permission context, fastMode, fileHistory, attribution, speculation state, remote-session status, repl-bridge status, thinking state, and much more. React consumers use useSyncExternalStore subscription.
Data flow for a single turn
┌─ user input (keyboard / stdin / websocket) ─┐
│ │
▼ │
processUserInput() ── slash cmd? ── local cmd ─┤── setMessages, maybe exit
│ │
▼ │
new UserMessage(s) pushed to mutableMessages │
│ │
▼ │
recordTranscript() → sessionStorage JSONL │
│ │
▼ │
┌─ query() generator ─────────────────────────┘
│ • compose system prompt (fetchSystemPromptParts)
│ • compose user/system context (getUserContext, getSystemContext)
│ • call claude.ts streaming
│ yields assistant blocks (text, thinking, tool_use)
│ • parallel execute tool_use via Tool.call()
│ yields tool_result blocks
│ • if max_output / fallback / 529 → recovery branch
│ • if threshold hit → auto-compact → continue
│ • if no tool_use → stop hooks → maybe retry → exit
└→ each yielded SDKMessage flows back to the UI or SDK consumer
Dependency direction
Broadly one-way:
entrypoints ─▶ main.tsx ─▶ QueryEngine ─▶ query ─▶ services/api + tools/* + services/compact
│ │
├─▶ components/UI ◀── hooks/* ───────┤
│ │
▼ ▼
state/ + context/ utils/*
▲ ▲
└─── constants/ + types/ (leaf) ────┘
Circular imports are avoided with lazy require() patterns and explicit comments — e.g. constants/product.ts:62-73 lazy-requires bridge/sessionIdCompat.ts to preserve "constants/ as leaf-of-DAG at module-load time." Likewise bootstrap/state.ts exists to hold module-level singletons without pulling in heavier modules.
What "one conversation" looks like as state
The critical running state for a session:
| Field | Kind | Lives in |
|---|---|---|
mutableMessages: Message[] |
Array of user/assistant/system/attachment messages | QueryEngine.mutableMessages or AppState.messages |
toolPermissionContext |
Permission mode + rules | AppState.toolPermissionContext |
readFileState: FileStateCache |
Hashes of every file read this turn (gates edits) | QueryEngine.readFileState |
fileHistoryState |
Snapshot-able history for /rewind |
AppState.fileHistory |
attributionState |
Tracks co-author attribution for commits | AppState.attribution |
totalUsage: NonNullableUsage |
Token accounting across all turns | QueryEngine.totalUsage |
permissionDenials |
Tool denials for SDK result.permission_denials |
QueryEngine.permissionDenials |
discoveredSkillNames |
Which skills the model has discovered this turn | QueryEngine.discoveredSkillNames |
loadedNestedMemoryPaths |
Nested CLAUDE.md files loaded as attachments | QueryEngine.loadedNestedMemoryPaths |
sessionId, parentSessionId |
Session lineage (plan → implement) | bootstrap/state.ts module-level |
cwd, originalCwd, projectRoot |
Working-directory tracking | bootstrap/state.ts module-level |
lastAPIRequest |
Last params (for /share bug reports) |
bootstrap/state.ts |
The transcript is appended to ~/.claude/sessions/<sessionId>/transcript.jsonl as it goes, so --resume can reconstruct everything.