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.
┌─────────────────────────────────────────────────┐
│ REPL / TUI Layer (Ink React Components) │ ← User sees this
│ main.tsx → interactiveHelpers.tsx → REPL.tsx │
├─────────────────────────────────────────────────┤
│ Query Engine Layer │ ← The brain
│ QueryEngine.ts → query.ts → queryLoop() │
├─────────────────────────────────────────────────┤
│ API & Tool Execution Layer │ ← The hands
│ claude.ts (API) + tools/ (43 tool impls) │
└─────────────────────────────────────────────────┘| 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:
1. Await MDM + keychain prefetches
2. init() — loads settings, config, auth
3. Set process.title = 'claude'
4. Initialize logging sinks
5. Run migrations (CURRENT_MIGRATION_VERSION = 11)
6. Load remote managed settings (non-blocking)
7. Upload user settings (if feature-gated)main.tsx:388-431)After first render, background work fires:
initUser(), getUserContext(), getSystemContext()This is the heart of Claude Code. Everything revolves around queryLoop().
User submits prompt
→ QueryEngine.submitMessage()
→ query() [outer generator]
→ queryLoop() [core infinite loop]
→ deps.callModel() [API call]
→ execute tools [tool pipeline]
→ loop again with resultsquery.ts:241+)Each iteration of queryLoop() follows this exact sequence:
┌──────────────────────────────────────────────────────────────┐
│ queryLoop() Iteration │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. Read current state (messages, toolUseContext, tracking) │
│ │
│ 2. Skill discovery prefetch (non-blocking, during streaming) │
│ │
│ 3. Yield 'stream_request_start' event │
│ │
│ 4. Increment query chain tracking (chainId, depth) │
│ │
│ 5. Get messages after compact boundary │
│ │
│ 6. Apply tool result budget (truncate oversized results) │
│ │
│ 7. Apply history snip (if enabled) │
│ │
│ 8. Apply microcompact (time-based or cached) │
│ │
│ 9. Apply context collapse (if enabled, at 90% usage) │
│ │
│ 10. Apply auto-compact (if context exceeds threshold) │
│ │
│ 11. Build full system prompt │
│ │
│ 12. Check blocking token limit (reject if too large) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ API CALL LOOP (with fallback retry) │ │
│ │ a. deps.callModel() → streams assistant response │ │
│ │ b. Collect assistantMessages, toolUseBlocks │ │
│ │ c. Handle streaming fallback (model switch on error) │ │
│ │ d. Withhold recoverable errors (PTL, max_tokens) │ │
│ │ e. Yield messages to consumer │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 13. Execute post-sampling hooks │
│ │
│ 14. Check for abort (user interrupt) │
│ │
│ 15. Yield previous turn's tool-use summary (if any) │
│ │
│ 16. ┌─ NO tool_use blocks? ──────────────────────────────┐ │
│ │ a. Handle withheld 413/max_tokens errors │ │
│ │ b. Run stop hooks │ │
│ │ c. Check token budget │ │
│ │ d. Return { reason: 'completed' } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 17. ┌─ HAS tool_use blocks? ─────────────────────────────┐ │
│ │ a. Execute tools (streaming or batched) │ │
│ │ b. Collect tool results │ │
│ │ c. Generate tool-use summary (async) │ │
│ │ d. Check abort mid-execution │ │
│ │ e. Collect attachments (queued commands, memory) │ │
│ │ f. Refresh tools (MCP servers) │ │
│ │ g. Check maxTurns │ │
│ │ h. Continue loop with new messages │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘The State type carries mutable state across loop iterations:
type State = {
messages: Message[] // Full conversation history
toolUseContext: ToolUseContext // Execution context (tools, abort, state)
autoCompactTracking: ... // Tracks compaction state
maxOutputTokensRecoveryCount: number // Recovery attempt counter
hasAttemptedReactiveCompact: boolean // Prevents infinite compact loops
pendingToolUseSummary: ... // Async summary from previous turn
turnCount: number // Iteration counter
transition: ... // Why previous iteration continued
}State is never mutated. Each loop continuation creates a brand new State object:
state = {
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
toolUseContext: toolUseContextWithQueryTracking,
turnCount: nextTurnCount,
// ...other fields
}This makes the loop deterministic, testable, and enables clean recovery paths.
queryLoop()
→ deps.callModel()
→ queryModelWithStreaming()
→ queryModel()
→ anthropic.beta.messages.create() [Anthropic SDK]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:
message_start → Initial message with usage
content_block_start → Beginning of text/tool_use/thinking block
content_block_delta → Streaming deltas (text chunks, tool input)
content_block_stop → Block completed
message_delta → Stop reason + final usage
message_stop → Request completeIf 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()runToolUse()
→ streamedCheckPermissionsAndCallTool()
→ checkPermissionsAndCallTool()The permission pipeline is strictly ordered:
1. Input validation → Zod schema parse (tool.inputSchema.safeParse())
2. Value validation → tool.validateInput() (e.g., blocked sleep patterns)
3. PreToolUse hooks → runPreToolUseHooks() (can modify input, block, decide)
4. Permission decision → resolveHookPermissionDecision() + canUseTool()
├── Permission mode check (default, plan, auto, bypass)
├── Always-allow rules (session, CLI, settings)
├── Always-deny rules
├── Auto-mode classifier (security check for Bash)
└── Permission hooks
5. If denied → Yield error tool_result, run PermissionDenied hooks
6. If allowed → Proceed to executiontool.call(input, toolUseContext, canUseTool, assistantMessage, onProgress)The tool's call() method executes with full context. Tools return:
type ToolResult<T> = {
data: T // The result data
newMessages?: Message[] // Optional additional messages to inject
contextModifier?: (ctx) => ctx // Function to update ToolUseContext
mcpMeta?: { ... } // MCP protocol metadata
}tool.mapToolResultToToolResultBlockParam() — Maps result to API formatmaxResultSizeChars)runPostToolUseHooks())createUserMessage({ content: [{ type: 'tool_result', ... }] })Tool results are appended to messagesForQuery and the loop continues:
state = {
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
// ...
}This triggers another API call with the tool results as context.
Tool.ts)Every tool implements the Tool<Input, Output, Progress> interface:
type Tool<Input, Output, Progress> = {
name: string // Unique identifier
inputSchema: ZodType<Input> // Input validation schema
description(...): Promise<string> // Dynamic description for the model
call(args, context, canUseTool, ...): Promise<ToolResult<Output>>
checkPermissions(input, context): Promise<PermissionResult>
isConcurrencySafe(input): boolean // Can run in parallel?
isReadOnly(input): boolean // Does it modify state?
isDestructive(input): boolean // Irreversible operation?
isEnabled(): boolean // Feature-gated?
validateInput?(input, context): ValidationResult
// ... plus ~30 more optional methods for UI rendering, progress, etc.
}Tools are created via buildTool() which fills in safe defaults:
const MyTool = buildTool({
name: 'MyTool',
inputSchema: z.object({ ... }),
description: async () => 'Does something useful',
call: async (args, ctx) => ({ data: 'result' }),
// ... only override what's needed
})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:
API streams: "I'll read file A and file B..."
→ ToolUse block for Read(fileA) starts streaming
→ Read(fileA) added to execution queue
→ ToolUse block for Read(fileB) starts streaming
→ Read(fileB) added to execution queue
→ Both Read tools execute in parallel (concurrency-safe)
API continues: "...and also run this command"
→ ToolUse block for Bash starts streaming
→ Bash added to queue (waits for serial execution)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)type TaskType =
| 'local_bash' // Shell command execution
| 'local_agent' // Subagent in same process
| 'remote_agent' // Agent in CCR (cloud) environment
| 'in_process_teammate' // Teammate in same process
| 'local_workflow' // Workflow execution
| 'monitor_mcp' // MCP server monitor
| 'dream' // Dream/experimental modeshouldRunAsync = run_in_background || selectedAgent.background ||
isCoordinator || forceAsync || assistantForceAsync ||
proactiveActive| 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:
1. registerAsyncAgent() — registers with AppState
2. createProgressTracker() — tracks execution progress
3. updateAsyncAgentProgress() — updates UI
4. Agent runs query() in background
5. On completion: enqueues <task-notification> XML to parent's message queue
6. Parent drains notifications via getCommandsByMaxPriority() filtered by agentIdWhen fork is enabled, extreme measures are taken for cache efficiency:
buildForkedMessages() creates:
[...history, assistant(all_tool_uses), user(placeholder_results..., per_child_directive)]┌─────────────────────────────────────────────────────┐
│ Bootstrap State (bootstrap/state.ts) │ ← Global singleton
│ Session ID, cost, telemetry, feature latches │
├─────────────────────────────────────────────────────┤
│ AppState Store (state/AppStateStore.ts) │ ← Immutable state object
│ Permissions, tasks, MCP, todos, notifications │
├─────────────────────────────────────────────────────┤
│ ToolUseContext (Tool.ts) │ ← Per-query execution context
│ Tools, abort controller, state accessors, agentId │
└─────────────────────────────────────────────────────┘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:
type AppState = {
toolPermissionContext: ToolPermissionContext
tasks: Map<string, TaskState>
mcp: { clients, tools, commands, resources }
todos: Map<AgentId, TodoItem[]>
notifications: Notification[]
agentNameRegistry: Map<string, AgentId>
settings: Settings
mainLoopModel: string
verbose: boolean
// ... many more fields
}state/store.ts)A minimal Zustand-like pub/sub store:
createStore(initialState, onChange?) → {
getState(), // Current state snapshot
setState(updater), // Functional update: (prev) => next
subscribe(listener) // Returns unsubscribe function
}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:
type ToolUseContext = {
options: {
commands: Command[]
tools: Tools
mainLoopModel: string
mcpClients: MCPServerConnection[]
agentDefinitions: AgentDefinitionsResult
// ...
}
abortController: AbortController
getAppState(): AppState
setAppState(f: (prev: AppState) => AppState): void
setAppStateForTasks?: (...) // Always-shared for session-scoped infrastructure
agentId?: AgentId // Subagent identity (undefined for main thread)
agentType?: string // Subagent type name
messages: Message[] // Current message array
readFileState: FileStateCache
// ... many more fields for UI, hooks, telemetry
}Each loop iteration creates a new State object (immutable pattern):
// At loop continuation:
state = {
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
toolUseContext: toolUseContextWithQueryTracking,
turnCount: nextTurnCount,
autoCompactTracking: updatedTracking,
// ...
}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 |
1. User message enters via QueryEngine.submitMessage(prompt)
2. processUserInput() handles slash commands, attachments, mode changes
3. Message pushed to mutableMessages and persisted to transcript
4. query() generator starts the agentic loop
5. Each API call produces assistant messages with optional tool_use blocks
6. Tool results become user messages with tool_result blocks
7. Loop continues until no tool_use blocks and stop hooks don't prevent
8. Final result yielded to QueryEngine which formats SDK responsenormalizeMessagesForAPI() 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:
Step 1: Rule-based checks
1a. Entire tool denied by rule → deny
1b. Entire tool has ask rule → ask (unless sandboxed Bash can auto-allow)
1c. Tool's own checkPermissions() → delegates to tool implementation
1d. Tool implementation denied → deny
1e. Tool requires user interaction → ask (bypass-immune)
1f. Content-specific ask rules → ask (bypass-immune)
1g. Safety checks (.git/, .claude/, .vscode/, shell configs) → ask (bypass-immune)
Step 2: Mode-based checks
2a. Bypass mode → allow
2b. Entire tool allowed by rule → allow
Step 3: Default
passthrough → converted to askFormat: ToolName(content) or just ToolName
Bash(git *) → matches git commands
Bash → matches all Bash
mcp__server1 → matches all tools from MCP server
mcp__server1__* → wildcard for server tools
Agent(Explore) → denies specific agent typeSources (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:
{
"continue": false,
"stopReason": "string",
"decision": "approve|block",
"systemMessage": "string",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"updatedInput": {},
"additionalContext": "..."
}
}Hooks use if conditions with patterns:
Bash(git *) → matches git commands
Write|Edit → matches Write or Edit
regex:.*\.ts$ → regex matchingThe 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 |
1. Memoized by getServerCacheKey(name, serverRef)
2. Creates transport based on type
3. Creates Client with capabilities: roots, elicitation
4. Sets ListRoots request handler (returns file://<cwd>)
5. Connects with timeout (default 30s)
6. On success: fetches capabilities, server version, instructions (capped at 2048 chars)
7. Sets up error/close handlers with reconnection logic
8. Registers elicitation handlermcp__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) |
---
name: my-skill
description: What this skill does
allowed-tools: Bash, Read, Write
argument-hint: <arg1> <arg2>
when_to_use: When to apply this skill
model: sonnet
user-invocable: true
disable-model-invocation: false
context: fork
paths: src/**/*.ts
effort: low
hooks:
PreToolUse:
- if: Bash(*)
command: ./hook.sh
---
Skill instructions here...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)Research (parallel workers) → Synthesis (coordinator) → Implementation (workers) → Verification (workers)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:
// Success
{ type: 'result', subtype: 'success', result: textResult, ... }
// Error
{ type: 'result', subtype: 'error_during_execution', errors: [...], ... }
// Budget exceeded
{ type: 'result', subtype: 'error_max_budget_usd', ... }
// Max turns
{ type: 'result', subtype: 'error_max_turns', ... }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:
policySettings > userSettings > projectSettings > localSettings > cliArg > command > sessionThis 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.