Claude Code — Agentic Loop & Architecture Deep Dive
A principal-engineer-level walkthrough of how Claude Code works end-to-end: the agentic loop, agent workflow, tool execution, state management, and everything in between.
Shared from "Claude-Code" on Inkdown
A principal-engineer-level walkthrough of how Claude Code works end-to-end: the agentic loop, agent workflow, tool execution, state management, and everything in between.
Claude Code is a TypeScript/React CLI application built with Ink (React for terminals). It runs on Bun and interfaces with the Anthropic API to create an agentic coding assistant.
| Component | File(s) | Role |
|---|---|---|
| Entry Point | main.tsx | CLI parsing, initialization, mode detection |
| Query Engine | QueryEngine.ts | High-level orchestrator, one per conversation |
| Agentic Loop | query.ts (queryLoop()) | The infinite loop that drives agent behavior |
| Tool Registry | Tool.ts, tools.ts | Tool type definitions and registry |
| State Store | state/store.ts, AppStateStore.ts | Zustand-like pub/sub store |
| API Client | services/api/claude.ts | Anthropic API calls with streaming |
| Bootstrap State | bootstrap/state.ts | Global singleton state (session, cost, telemetry) |
main.tsx:1-20)Before any heavy imports load, three parallel prefetches fire:
profileCheckpoint('main_tsx_entry') — Marks startup timingstartMdmRawRead() — Fires MDM subprocesses (macOS config reads)startKeychainPrefetch() — Fires macOS keychain reads (OAuth + API key)These run in parallel with the ~135ms of module evaluation that follows, eliminating sequential I/O bottlenecks.
main.tsx:884+)The app uses Commander.js to parse CLI arguments. Key modes:
| Mode | Flag | Behavior |
|---|---|---|
| Interactive REPL | (default) | Full TUI with Ink React |
| Headless/Print | -p / --print | Non-interactive, output to stdout |
| SDK Mode | --sdk-url | SDK consumer drives the session |
| SSH Remote | ssh <host> | Remote execution via SSH tunnel |
| Direct Connect | cc://... | Connect to a running Claude Code instance |
| Assistant Mode | assistant [id] | Kairos/assistant feature (feature-gated) |
main.tsx:907-967)Runs once before any command executes:
main.tsx:388-431)After first render, background work fires:
initUser(), getUserContext(), getSystemContext()This is the heart of Claude Code. Everything revolves around queryLoop().
query.ts:241+)Each iteration of queryLoop() follows this exact sequence:
The State type carries mutable state across loop iterations:
State is never mutated. Each loop continuation creates a brand new State object:
This makes the loop deterministic, testable, and enables clean recovery paths.
services/api/claude.ts:752+)queryModel() builds a BetaMessageStreamParams object:
| Parameter | Source | Purpose |
|---|---|---|
model | getRuntimeMainLoopModel() | Resolved from permission mode + token count |
system | System prompt construction | Full system prompt with cache breakpoints |
messages | normalizeMessagesForAPI() | Normalized conversation history |
tools | toolToAPISchema() | Tool schemas (Zod → JSON Schema) |
thinking | Thinking config | Adaptive/disabled thinking |
max_tokens | Model config | Output token limit |
temperature, top_p, top_k | Model config | Sampling parameters |
betas | Feature flags | Prompt caching, context management, structured outputs |
metadata | Session state | User ID, session ID for tracking |
The API uses Anthropic's MessageStream for server-sent events:
If a FallbackTriggeredError occurs (529, rate limit, etc.):
fallbackModel (e.g., Sonnet → Opus)| Error | Recovery Strategy |
|---|---|
| Prompt too long (413) | Context collapse drain → Reactive compact → Surface error |
| Max output tokens | Escalate to 64k (once) → Inject "Resume directly" message → Retry 3x → Surface |
| Media size error | Strip media → Retry once → Surface |
| Structured output retry | Retry with relaxed constraints → Surface after limit |
This is where the agent acts on the world. Five phases:
Two execution modes exist, controlled by config.gates.streamingToolExecution:
StreamingToolExecutor (enabled):
siblingAbortControllerrunTools() (fallback):
partitionToolCalls() groups consecutive concurrency-safe tools into batchesrunToolsConcurrently() (up to 10 parallel, configurable via CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY)runToolsSerially()The permission pipeline is strictly ordered:
The tool's call() method executes with full context. Tools return:
tool.mapToolResultToToolResultBlockParam() — Maps result to API formatmaxResultSizeChars)runPostToolUseHooks())createUserMessage({ content: [{ type: 'tool_result', ... }] })Tool results are appended to messagesForQuery and the loop continues:
This triggers another API call with the tool results as context.
Tool.ts)Every tool implements the Tool<Input, Output, Progress> interface:
Tools are created via buildTool() which fills in safe defaults:
Defaults (fail-closed where it matters):
isEnabled → trueisConcurrencySafe → false (assume not safe)isReadOnly → false (assume writes)isDestructive → falsecheckPermissions → { behavior: 'allow' } (defer to general system)toAutoClassifierInput → '' (skip classifier — security tools must override)tools.ts)All tools are assembled into a Tools array (readonly Tool[]) and passed through the ToolUseContext. The tool pool is:
.claude/agents/| Tool | File | What It Does |
|---|---|---|
| Bash | tools/BashTool/ | Execute shell commands with sandbox, timeout, backgrounding |
| Read | tools/FileReadTool/ | Read files (text, images, PDFs, notebooks) with dedup |
| Edit | tools/FileEditTool/ | String replacement in files with staleness checks |
| Write | tools/FileWriteTool/ | Write/overwrite files with read-first requirement |
| Agent | tools/AgentTool/ | Spawn subagents (sync, async, fork, remote, teammate) |
| Glob | tools/GlobTool/ | File pattern matching |
| Grep | tools/GrepTool/ | Content search with ripgrep |
| TodoWrite | tools/TodoWriteTool/ | Task tracking panel |
| WebSearch | tools/WebSearchTool/ | Web search via Exa |
| WebFetch | tools/WebFetchTool/ | Fetch URL content |
| Skill | tools/SkillTool/ | Execute skill commands |
| MCP | tools/MCPTool/ | Execute MCP server tools |
| ToolSearch | tools/ToolSearchTool/ | Deferred tool loading (lazy schema loading) |
When a tool result exceeds maxResultSizeChars:
When streamingToolExecution is enabled, tools start executing while the API response is still streaming:
This reduces latency significantly for independent tool calls.
The Agent tool is the primary mechanism for spawning nested agents. It supports multiple spawn modes:
Task.ts)| Mode | Behavior | Use Case |
|---|---|---|
| Sync | Blocks parent's turn, shares abort controller | Quick delegations |
| Async | Independent lifecycle, own abort controller | Long-running tasks |
Teammate (Agent Swarms):
team_name + name providedspawnTeammate() → launches in tmux split-pane with its own processRemote Agent:
isolation: 'remote'teleportToRemote() → launches in CCR (cloud) environmentFork Subagent (experiment, feature-gated):
subagent_type omitted and fork gate enabledStandard Subagent:
runAgent() → calls query() recursivelyEach subagent gets an isolated execution environment:
getSystemPrompt())Async agents are managed by LocalAgentTask:
When fork is enabled, extreme measures are taken for cache efficiency:
buildForkedMessages() creates:
bootstrap/state.ts)A global STATE singleton containing:
sessionId, parentSessionId, projectRoot, originalCwdtotalCostUSD, per-model modelUsagepromptCache1hEligible, afkModeHeaderLatched, etc.registeredHooks by event typeagentColorMap, invokedSkillsstate/AppStateStore.ts)A massive immutable state object:
state/store.ts)A minimal Zustand-like pub/sub store:
Key design: setState(updater) does identity checking to skip no-op updates, preventing unnecessary re-renders.
The per-query execution context passed to every tool:
Each loop iteration creates a new State object (immutable pattern):
This ensures clean state snapshots at each transition point and enables deterministic recovery.
types/message.ts)| Type | Purpose |
|---|---|
user | User input, tool results, meta messages |
assistant | LLM responses with text/thinking/tool_use blocks |
system | Internal messages (compact_boundary, api_error, local_command) |
progress | Tool execution progress updates |
attachment | System attachments (file changes, task notifications, max_turns) |
tombstone | Control signal to remove messages from UI |
stream_event | Raw API streaming events |
tool_use_summary | Haiku-generated summaries of tool batches |
normalizeMessagesForAPI() strips UI-only messages before sending to the API. The API only sees:
UI-only messages (system messages, progress messages, tombstones) are excluded.
Mid-turn attachments inject additional context into the conversation:
| Mode | Behavior |
|---|---|
default | Normal prompting for each tool use |
plan | Plan mode — pauses before execution |
acceptEdits | Auto-accepts safe edits |
bypassPermissions | All tools allowed without prompting |
dontAsk | Denies anything that would prompt |
auto (ant-only) | AI classifier auto-approves/denies |
bubble | Internal subagent mode |
hasPermissionsToUseTool)The decision pipeline is strictly ordered:
Format: ToolName(content) or just ToolName
Sources (priority order): policySettings, userSettings, projectSettings, localSettings, cliArg, command, session
When mode is auto and result is ask:
acceptEdits mode would allow → auto-approveclassifyYoloAction() with transcript + formatted action
When shouldAvoidPermissionPrompts (background/subagent):
PermissionRequest hooks firstHooks are user-defined shell commands (or JS callbacks) executed at specific lifecycle points. They are defined in:
.claude/settings.jsontypes/hooks.ts)| Event | When It Fires | What It Can Do |
|---|---|---|
| PreToolUse | Before any tool executes | Modify input, change permission, block |
| PostToolUse | After successful tool execution | Inject context, modify MCP output |
| PostToolUseFailure | After tool failure | React to failures |
| SessionStart | At session start | Inject context, register watchPaths |
| Setup | During setup phase | Initialize state |
| Stop | After model finishes (no tool_use) | Prevent continuation, inject errors |
| StopFailure | When model errors | Silent notification |
| SubagentStart/Stop | Around subagent lifecycle | Track subagent activity |
| UserPromptSubmit | When user submits a prompt | Inject additional context |
| PermissionDenied | When permission is denied | Retry with different input |
| PermissionRequest | For headless agents | Allow/deny without UI |
| CwdChanged | When working directory changes | Register new watchPaths |
| FileChanged | When watched files change | React to file changes |
| WorktreeCreate | When git worktree created | Setup worktree state |
| Notification | For system notifications | Handle notifications |
| ConfigChange | When configuration changes | Reload config |
| TaskCreated/Completed | Around background task lifecycle | Track tasks |
| TeammateIdle | When a teammate is idle | React to idle state |
Hooks execute as child processes:
bash (Git Bash on Windows, /bin/sh elsewhere). PowerShell hooks use pwshexecuteInBackground(), results delivered later via task-notificationhookJSONOutputSchemaTOOL_HOOK_EXECUTION_TIMEOUT_MS)Hooks return JSON:
Hooks use if conditions with patterns:
The prepareIfConditionMatcher function does expensive work (tree-sitter parsing for Bash) once, then the closure is called per hook.
Called from query.ts after model finishes with no tool_use. Can:
preventContinuation: true to end the turnblockingErrors to inject error messages and re-enter the loopstopHookActive: true to prevent death spiralsThe query loop applies context management in this order each iteration:
HISTORY_SNIP feature)snipTokensFreed which is subtracted from token countsTwo implementations:
Time-based microcompact (active path):
gapThresholdMinutesCached microcompact (CACHED_MICROCOMPACT feature, ant-only):
cache_edits feature to delete tool results without invalidating the cached prefixcachedMCState, queues cache_edits blocksCONTEXT_COLLAPSE feature)projectView() replayrecoverFromOverflow() drains staged collapses on real API 413effectiveContextWindow - 13,000 tokens bufferREACTIVE_COMPACT feature)prompt_too_long errors and fires compact as recoveryreadFileState cacheloadedNestedMemoryPathssentSkillNames (saves ~4K tokens)| Threshold | Tokens Below Window | Behavior |
|---|---|---|
| Warning | 20K | Warning shown to user |
| Error | 20K | Error state entered |
| Blocking limit | 3K | Hard block — no more API calls |
| Manual compact buffer | 3K | Reserved for /compact command |
Model Context Protocol (MCP) allows Claude Code to connect to external tool servers that provide additional tools, resources, and prompts.
| Transport | How It Works |
|---|---|
| stdio | Spawns subprocess, communicates via stdin/stdout |
| sse | Server-Sent Events with auth provider |
| http | Streamable HTTP with OAuth, session management |
| ws | WebSocket with TLS/mTLS options |
| claudeai-proxy | Routes through claude.ai OAuth proxy |
| in-process | Chrome MCP and Computer Use run in-process |
mcp__serverName__toolNameuseMergedToolsmcp__ide__executeCode and mcp__ide__getDiagnostics allowedtruncateMcpContentIfNeededpersistToolResultSkills are markdown files with YAML frontmatter that become Command objects. They provide reusable capabilities that Claude can discover and execute.
| Location | Scope |
|---|---|
| Bundled | Compiled into CLI binary |
| User | ~/.claude/skills/ |
| Project | .claude/skills/ (walks up to home) |
| Managed | Policy-managed path |
| Plugin | From plugin directories |
| MCP | From MCP server skill builders |
| Legacy | .claude/commands/ (deprecated) |
parseFrontmatter() extracts YAML + markdown contentrealpath (handles symlinks), first-wins orderingpaths frontmatter stored separately, activated when matching files are touched.claude/skills dirsgetPromptForCommand: Substitutes arguments, replaces ${CLAUDE_SKILL_DIR} and ${CLAUDE_SESSION_ID}, executes inline shell commands (!...`)substituteArguments()A feature-gated mode (CLAUDE_CODE_COORDINATOR_MODE=1) that transforms Claude into a task orchestrator rather than a direct code editor.
The coordinator receives a specialized system prompt instructing it to:
subagent_type: 'worker'<task-notification> messagesWorkers get a restricted tool set:
ASYNC_AGENT_ALLOWED_TOOLS minus internal tools (TeamCreate, TeamDelete, SendMessage, SyntheticOutput)getCoordinatorUserContext() injects a workerToolsContext into the system prompt telling the coordinator which tools its workers have access to.
The loop terminates via return { reason: ... } at multiple points:
| Reason | When |
|---|---|
completed | No tool_use blocks, stop hooks pass, no blocking errors |
stop_hook_prevented | Stop hook indicated not to continue |
| Reason | When |
|---|---|
max_turns | Exceeded maxTurns parameter |
blocking_limit | Context exceeds hard token limit |
| Budget exceeded | Checked in QueryEngine via getTotalCost() >= maxBudgetUsd |
| Reason | When |
|---|---|
model_error | API call threw an exception |
prompt_too_long | Recovery exhausted for 413 errors |
image_error | Media size error recovery exhausted |
error_max_structured_output_retries | Structured output retry limit |
| Reason | When |
|---|---|
aborted_streaming | Abort signal fired during API streaming |
aborted_tools | Abort signal fired during tool execution |
hook_stopped | Hook indicated to prevent continuation |
When queryLoop returns, QueryEngine.submitMessage() maps the terminal state to an SDK result:
The entire loop is an async generator, yielding events/messages as they happen. This enables:
State is never mutated; new State objects are created at each continue site. This makes:
Subagents call query() recursively, creating nested agentic loops. Each has:
Tools can start executing while the API response is still streaming, reducing latency for independent tool calls. This is a significant performance optimization.
Most advanced features are behind feature() gates with conditional require() for dead code elimination:
The fork subagent path goes to extreme lengths to maximize Anthropic's prompt cache hit rate:
The loop has multiple recovery paths before surfacing errors:
ALL hooks, git operations, and system context reads require workspace trust to be established first. This prevents arbitrary code execution before the user has explicitly trusted the workspace.
Tool results that exceed size limits are persisted to disk with a preview shown to the model. This keeps the context window manageable while preserving full output accessibility.
Permission rules come from multiple sources with clear priority ordering:
This allows enterprise policies to override user preferences, which override project defaults, etc.
| File | Role |
|---|---|
main.tsx | Entry point, CLI parsing, initialization |
QueryEngine.ts | High-level conversation orchestrator |
query.ts | The agentic loop (queryLoop()) |
query/deps.ts | Production dependencies injected into query |
Tool.ts | Tool type definitions and buildTool() |
tools.ts | Tool registry assembly |
tools/ | Individual tool implementations (43 tools) |
state/store.ts | Zustand-like pub/sub store |
state/AppStateStore.ts | Immutable AppState type and store |
bootstrap/state.ts | Global singleton state |
Task.ts | Task type definitions and ID generation |
tasks.ts | Task management utilities |
types/message.ts | Message type definitions |
types/hooks.ts | Hook event type definitions |
types/permissions.ts | Permission type definitions |
utils/permissions/ | Permission system implementation |
utils/hooks.ts | Hook execution engine |
services/api/claude.ts | Anthropic API client |
services/mcp/ | MCP server integration |
skills/ | Skills system |
coordinator/ | Coordinator mode |
context/ | React contexts (notifications, modals, etc.) |
This document covers the core agentic loop, agent workflow, tool execution, state management, permission system, hooks, context management, MCP integration, skills, and coordinator mode. The codebase is ~100K+ lines of TypeScript with heavy use of feature flags, generators, and immutable state patterns.