Comprehensive analysis of the
cc-srcrepository
Date: 2026-04-13
- Executive Summary
- System Architecture Overview
- Core Components Deep Dive
- 3.1 The Agentic Query Loop
- 3.2 Tool System
- 3.3 Permission System
- 3.4 State Management
- 3.5 MCP Integration
- 3.6 Hook System
- 3.7 Command System
- 3.8 Multi-Agent Coordination
- Key Engineering Best Practices
- Critical Success Factors
- Technology Stack
- Lessons Learned
Claude Code is Anthropic's official CLI for Claude — a production-grade AI agent built on top of the Claude API. Unlike most "chatbot wrappers," Claude Code is a fully autonomous software engineering agent: it reads files, runs shells, edits code, searches the web, spawns sub-agents, manages long-running tasks, and integrates with external tools via MCP — all within a streaming, turn-based loop designed for safety and extensibility.
The source reveals a codebase of extraordinary sophistication: ~4,683 lines in main.tsx alone, 290+ utility modules, 43+ built-in tools, 50+ commands, 87+ React hooks, and a plugin/MCP integration layer. Yet it maintains coherent architecture through disciplined abstractions.
The #1 insight from this codebase: a great agent is not just a "model + tools" — it's a carefully engineered system for trust, extensibility, context management, and graceful failure.
graph TB
subgraph "User Interfaces"
CLI[CLI REPL<br/>Ink/React]
SDK[Agent SDK<br/>Programmatic]
Bridge[Claude.ai Bridge<br/>iframe sync]
IDE[IDE Extension<br/>VS Code / JetBrains]
end
subgraph "Entry & Initialization"
INIT[init.ts<br/>15-phase startup pipeline]
MAIN[main.tsx<br/>CLI orchestrator]
end
subgraph "Core Agentic Loop"
QUERY[query.ts<br/>AsyncGenerator loop]
CTX[context.ts<br/>System prompt builder]
HIST[history.ts<br/>Conversation history]
ATTACH[Attachment System<br/>File changes, memory, skills]
end
subgraph "Intelligence Layer"
API[Anthropic API<br/>claude-sonnet/opus/haiku]
CACHE[Prompt Cache<br/>cache_control blocks]
TOKEN[Token Budget<br/>tracking & compaction]
COMPACT[Context Compactor<br/>message pruning]
end
subgraph "Tool Execution"
TOOLS[Tool Registry<br/>43+ tools]
PERM[Permission Engine<br/>rules + classifier]
HOOKS[Hook Runner<br/>PreToolUse, PostToolUse]
EXEC[Concurrent Executor<br/>parallel tool calls]
end
subgraph "State & Tasks"
APPSTATE[AppState Store<br/>DeepImmutable]
TASKS[Task Manager<br/>background agents]
TASKOUT[Task Output<br/>disk-streamed]
end
subgraph "Extensions"
MCP[MCP Layer<br/>stdio/SSE/HTTP/WS]
PLUGINS[Plugin System]
SKILLS[Skill Discovery]
CMDS[Command Registry<br/>50+ commands]
end
subgraph "Infrastructure"
GIT[Git Integration]
BASH[Shell Execution]
LSP[Language Server]
FS[File State Cache]
OAUTH[OAuth Flows]
GB[GrowthBook<br/>Feature Flags]
OTEL[OpenTelemetry<br/>Observability]
end
CLI --> MAIN
SDK --> MAIN
Bridge --> MAIN
IDE --> MAIN
MAIN --> INIT
INIT --> QUERY
QUERY --> CTX
QUERY --> HIST
QUERY --> ATTACH
QUERY --> API
API --> CACHE
QUERY --> TOKEN
TOKEN --> COMPACT
API -->|tool_use blocks| TOOLS
TOOLS --> PERM
PERM --> HOOKS
HOOKS --> EXEC
EXEC -->|results| QUERY
APPSTATE --> QUERY
TASKS --> APPSTATE
EXEC --> TASKS
TASKS --> TASKOUT
TOOLS --> MCP
TOOLS --> PLUGINS
TOOLS --> SKILLS
MAIN --> CMDS
EXEC --> BASH
EXEC --> GIT
EXEC --> LSP
EXEC --> FS
INIT --> OAUTH
INIT --> GB
QUERY --> OTEL
sequenceDiagram
participant User
participant REPL
participant Query
participant API as Anthropic API
participant Tools
participant Perm as Permissions
participant Hooks
User->>REPL: Enter prompt
REPL->>REPL: UserPromptSubmit hook
REPL->>Query: query(params)
Query->>Query: normalizeMessagesForAPI()
Query->>Query: inject attachments (memory, file changes, skills)
Query->>Query: build system prompt via context.ts
Query->>API: POST /messages (streaming)
loop Streaming turn
API-->>Query: stream delta (text / tool_use)
Query-->>REPL: yield StreamEvent (live display)
end
API-->>Query: stop_reason: tool_use
loop For each tool call (concurrent)
Query->>Perm: checkPermissions(tool, input)
alt Permission denied
Perm-->>User: prompt dialog
User-->>Perm: approve/deny
end
Perm->>Hooks: PreToolUse event
Hooks->>Tools: call(input, context)
Tools-->>Hooks: result
Hooks->>Hooks: PostToolUse event
Hooks-->>Query: tool_result
end
Query->>Query: checkTokenBudget()
alt Near context limit
Query->>Query: compact(messages)
end
Query->>API: continue with tool_results
API-->>Query: stop_reason: end_turn
Query-->>REPL: final AssistantMessage
REPL-->>User: display response
File: query.ts (~1,729 lines)
The heart of Claude Code is an AsyncGenerator-based loop that orchestrates every turn of the conversation.
flowchart TD
START([Start query]) --> NORM[normalizeMessagesForAPI]
NORM --> ATTACH[Inject attachments\nmemory · file changes · skills]
ATTACH --> CTXBUILD[Build system prompt\ncontext.ts]
CTXBUILD --> APISTREAM[Stream API call\nclaude.ts]
APISTREAM --> DELTA{delta type?}
DELTA -->|text| YIELD_TEXT[yield TextEvent]
DELTA -->|tool_use| COLLECT[Collect tool calls]
DELTA -->|stop| STOP_REASON{stop_reason?}
COLLECT --> PERM_CHECK[checkPermissions per tool]
PERM_CHECK --> CONCURRENT[Run tools concurrently]
CONCURRENT --> RESULTS[Collect tool results]
RESULTS --> TOKEN_CHECK{Token budget OK?}
TOKEN_CHECK -->|over budget| COMPACT[compact messages]
TOKEN_CHECK -->|OK| CONTINUE[Append tool_results]
COMPACT --> CONTINUE
CONTINUE --> APISTREAM
STOP_REASON -->|tool_use| PERM_CHECK
STOP_REASON -->|end_turn| YIELD_MSG[yield AssistantMessage]
STOP_REASON -->|max_tokens| AUTO_CONTINUE[Auto-continue\nvia synthetic tool]
AUTO_CONTINUE --> APISTREAM
YIELD_MSG --> END([End])
YIELD_TEXT --> APISTREAM
Key design decisions:
- Generator-based (
AsyncGenerator<Message | StreamEvent>) — callers can consume events in real-time without buffering - Depth tracking prevents infinite recursion in sub-agents
- Automatic continuation on
max_tokens— model never truly "runs out" - Token budgets enforced per-turn, not just cumulative
Files: Tool.ts, tools.ts, /tools/
Every capability in Claude Code is a Tool. This single abstraction unifies file I/O, shell execution, web search, sub-agent spawning, MCP invocations, and more.
classDiagram
class Tool {
+name: string
+description: string
+inputSchema: ZodSchema
+isEnabled: () => boolean
+isReadOnly: () => boolean
+shouldDefer: boolean
+checkPermissions(input, ctx): PermissionResult
+validateInput(input): ValidationResult
+call(input, ctx): AsyncGenerator~ToolResult~
+renderToolResultMessage(result, input): ReactNode
}
class ToolResult {
+type: "text" | "image" | "resource" | ...
+content: string | Buffer
+metadata?: object
}
class PermissionResult {
+result: "allow" | "deny" | "ask"
+reason?: string
+updatedInput?: object
}
Tool --> ToolResult
Tool --> PermissionResult
class BashTool
class FileEditTool
class FileReadTool
class GlobTool
class GrepTool
class AgentTool
class MCPTool
class WebSearchTool
class TaskCreateTool
class EnterPlanModeTool
class SleepTool
class MonitorTool
class AskUserQuestionTool
Tool <|-- BashTool
Tool <|-- FileEditTool
Tool <|-- FileReadTool
Tool <|-- GlobTool
Tool <|-- GrepTool
Tool <|-- AgentTool
Tool <|-- MCPTool
Tool <|-- WebSearchTool
Tool <|-- TaskCreateTool
Tool <|-- EnterPlanModeTool
Tool <|-- SleepTool
Tool <|-- MonitorTool
Tool <|-- AskUserQuestionTool
Tool Lifecycle:
stateDiagram-v2
[*] --> Registered: buildTool() + getTools()
Registered --> Selected: model emits tool_use block
Selected --> Validated: validateInput()
Validated --> PermChecked: checkPermissions()
PermChecked --> UserPrompted: result = "ask"
UserPrompted --> Approved: user approves
UserPrompted --> Denied: user denies
PermChecked --> Approved: result = "allow"
PermChecked --> Denied: result = "deny"
Approved --> HookPre: PreToolUse hook
HookPre --> Executing: call(input, ctx)
Executing --> Streaming: yield partial results
Streaming --> Executing: more results
Executing --> HookPost: PostToolUse hook
HookPost --> Rendered: renderToolResultMessage()
Rendered --> [*]
Denied --> [*]
Best practice: deferred tools. Non-critical tools are marked shouldDefer: true. The model first receives a stub ToolSearch tool and must explicitly fetch deferred tools before calling them. This keeps the initial context window lean.
Files: /utils/permissions/, Tool.checkPermissions()
Claude Code has a four-layer permission model that balances automation with safety:
flowchart LR
subgraph "Layer 1: Static Rules"
AR[alwaysAllowRules\ne.g. Bash(git status)]
AD[alwaysDenyRules\ne.g. Bash(rm -rf /)]
AA[alwaysAskRules\ne.g. Bash(curl *)]
end
subgraph "Layer 2: Tool-Level"
TC[Tool.checkPermissions\nper-tool logic]
end
subgraph "Layer 3: Hooks"
PRE[PreToolUse hook\ncustom policy code]
end
subgraph "Layer 4: User Dialog"
DLG[Interactive prompt\napprove/deny/always]
end
subgraph "Layer 5: ML Classifier"
CLS[async classifier\nhigh-risk detection]
end
INPUT[Tool call] --> AR
AR -->|match allow| EXEC[Execute]
AR -->|match deny| BLOCK[Block]
AR -->|no match| AD
AD -->|match deny| BLOCK
AD -->|no match| TC
TC -->|allow| PRE
TC -->|deny| BLOCK
TC -->|ask| AA
AA -->|match ask| DLG
AA -->|no match| PRE
PRE -->|allow| EXEC
PRE -->|block| BLOCK
PRE -->|ask| DLG
DLG -->|high risk| CLS
CLS -->|approved| EXEC
CLS -->|rejected| BLOCK
DLG -->|user approves| EXEC
DLG -->|user denies| BLOCK
Modes:
| Mode | Behavior |
|---|---|
default |
Ask user for each new tool call pattern |
plan |
Require plan approval before any execution |
bypass |
Auto-approve everything (CI/scripts) |
auto |
ML classifier decides automatically |
Files: /state/AppStateStore.ts, /state/AppState.tsx
Claude Code uses a custom Zustand-like store built on useSyncExternalStore. The entire application state is DeepImmutable<AppState>, preventing accidental mutation.
graph TD
subgraph "AppState (DeepImmutable)"
UI[Display State\nmodel · verbose · spinnerTip]
PERM_STATE[Permission Context\ntoolPermissionContext]
MCP_STATE[MCP State\nclients · tools · resources]
TASK_STATE[Task Registry\ntaskId → TaskState]
AGENT_STATE[Agent Registry\nname → agentId]
SETTINGS[Settings\nJSON config]
BRIDGE_STATE[Bridge State\nreplBridgeEnabled · connected]
REMOTE_STATE[Remote Session\nurl · connectionStatus]
PLUGIN_STATE[Plugin State\nenabled · errors]
end
subgraph "Store Pattern"
CREATE[createStore(initialState)]
UPDATE[store.update(fn)]
SUBSCRIBE[store.subscribe(listener)]
GET[store.getState()]
end
subgraph "React Integration"
HOOK[useAppState(selector)]
SYNC[useSyncExternalStore]
CONTEXT[React Context Provider]
end
CREATE --> UPDATE
UPDATE -->|immutable diff| SUBSCRIBE
SUBSCRIBE --> SYNC
SYNC --> HOOK
CONTEXT --> HOOK
GET --> HOOK
Why DeepImmutable? Prevents the class of bugs where a deeply nested mutation goes undetected because object identity didn't change — critical in an async streaming environment.
Files: /services/mcp/
MCP (Model Context Protocol) is Claude Code's extensibility backbone — allowing any external process to expose tools and resources.
graph TB
subgraph "Config Sources"
LOCAL[Local: .claude/mcp.json]
USER[User: ~/.claude/config.json]
PROJECT[Project: .claude-code/claude.json]
ENTERPRISE[Enterprise: MDM / remote settings]
MANAGED[Managed: remote registry]
end
subgraph "Transport Layer"
STDIO[stdio\nsubprocess]
SSE[SSE\nServer-Sent Events]
HTTP[HTTP\nREST streaming]
WS[WebSocket\nbidirectional]
SDK_TRANS[SDK\ninternal agent]
end
subgraph "MCP Client"
DISCOVER[Tool Discovery\nlistTools()]
RESOURCE[Resource Discovery\nlistResources()]
INVOKE[Tool Invocation\ncallTool()]
AUTH[OAuth / XAA\nper-server auth]
end
subgraph "Claude Integration"
WRAP[MCPTool wrapper\nfor each MCP tool]
RDTOOL[ReadMcpResourceTool]
LSTOOL[ListMcpResourcesTool]
end
LOCAL --> STDIO
USER --> SSE
PROJECT --> HTTP
ENTERPRISE --> WS
MANAGED --> SDK_TRANS
STDIO --> DISCOVER
SSE --> DISCOVER
HTTP --> DISCOVER
WS --> DISCOVER
SDK_TRANS --> DISCOVER
DISCOVER --> WRAP
RESOURCE --> RDTOOL
RESOURCE --> LSTOOL
INVOKE --> AUTH
WRAP --> INVOKE
Transport comparison:
| Transport | Use case | Notes |
|---|---|---|
stdio |
Local CLI tools | Subprocess spawned by Claude Code |
sse |
Remote HTTP services | One-way push from server |
http |
REST-based MCP | Stateless per request |
ws |
Real-time remote | Bidirectional streaming |
sdk |
Internal agents | Agent SDK controlled |
Files: /utils/hooks/, /types/hooks.ts
Hooks provide event-driven extensibility without tight coupling — the extension model for Claude Code.
sequenceDiagram
participant Config as settings.json / hooks/
participant Runner as HookRunner
participant Event as Hook Event Bus
participant Tool as Tool Execution
participant Handler as User Hook Script
Config->>Runner: register hook handlers
Note over Runner,Tool: On every tool call:
Tool->>Event: emit PreToolUse(toolName, input, messages)
Event->>Handler: execute handler subprocess/function
Handler-->>Event: { decision: "approve" | "block", updatedInput? }
Event-->>Tool: permission result + possible input transform
Tool->>Tool: execute tool
Tool->>Event: emit PostToolUse(toolName, input, output)
Event->>Handler: execute handler
Handler-->>Event: { additionalContext? }
Event-->>Tool: optional result augmentation
Available hook events:
| Event | Trigger | Can Do |
|---|---|---|
SessionStart |
Session initialization | Setup, env validation |
UserPromptSubmit |
Before message → query | Rewrite, augment, block |
PreToolUse |
Before tool execution | Approve/block/transform input |
PostToolUse |
After tool execution | Transform output, log |
FileChanged |
File system change | Trigger actions, invalidate cache |
Files: commands.ts, /commands/
Commands are user-facing slash actions (/config, /plan, /commit, etc.):
graph LR
subgraph "Command Sources"
BUNDLED[Bundled commands\n50+ built-in]
SKILL[Skills\n.claude-commands.yml]
MCP_CMD[MCP commands\nfrom connected servers]
PLUGIN_CMD[Plugin commands]
end
subgraph "Command Types"
PROMPT[prompt\nExpanded into message\nsent to Claude]
LOCAL[local\nSync execution\nreturns text]
LOCAL_JSX[local-jsx\nRenders React UI\nin terminal]
end
subgraph "Examples"
EX1[/commit → prompt\nAI writes commit msg]
EX2[/config → local-jsx\nSettings UI rendered]
EX3[/plan → local\nEnter plan mode]
EX4[/doctor → local\nHealth check output]
end
BUNDLED --> PROMPT
BUNDLED --> LOCAL
BUNDLED --> LOCAL_JSX
SKILL --> PROMPT
MCP_CMD --> PROMPT
PLUGIN_CMD --> LOCAL
PROMPT --> EX1
LOCAL_JSX --> EX2
LOCAL --> EX3
LOCAL --> EX4
Files: /tools/AgentTool/, /coordinator/, /tasks/
Claude Code supports spawning sub-agents for parallel or isolated work:
graph TB
subgraph "Orchestrator Agent"
ORC[Main query loop]
AGENT_TOOL[AgentTool.call]
TASK_REG[Task Registry\nAppState.tasks]
end
subgraph "Sub-agent: Worktree Isolated"
WT[Git Worktree\nisolated branch]
SUB_LOOP[Sub-agent query loop]
SUB_TOOLS[Own tool set\nOwn file state cache]
SUB_PERM[Own permission context]
end
subgraph "Sub-agent: In-process Teammate"
TP[In-process thread]
TP_LOOP[Teammate query loop]
TP_MAIL[Mailbox\nbidirectional messages]
end
subgraph "Remote Agent"
RA[Remote session URL]
RA_WS[WebSocket connection]
RA_LOOP[Remote query loop]
end
ORC -->|spawn| AGENT_TOOL
AGENT_TOOL -->|isolation=worktree| WT
AGENT_TOOL -->|isolation=thread| TP
AGENT_TOOL -->|isolation=remote| RA
WT --> SUB_LOOP
SUB_LOOP --> SUB_TOOLS
SUB_LOOP --> SUB_PERM
TP --> TP_LOOP
TP_LOOP <-->|SendMessage| TP_MAIL
TP_MAIL <-->|SendMessage| ORC
RA --> RA_WS
RA_WS --> RA_LOOP
TASK_REG -.->|monitor| SUB_LOOP
TASK_REG -.->|monitor| TP_LOOP
TASK_REG -.->|monitor| RA_LOOP
Isolation guarantees per mode:
| Isolation | File State | Git State | Permission Context | Communication |
|---|---|---|---|---|
| Worktree | Own copy | Own branch | Inherited | Via task output |
| Thread (teammate) | Shared | Shared | Inherited | Bidirectional mailbox |
| Remote | Own process | Own env | Independent | WebSocket |
Practice: Every capability is a Tool with a consistent interface: validateInput → checkPermissions → call → renderResult.
Why it works: New capabilities don't require changes to the core loop. The model discovers and calls tools via function calling — adding a tool is adding one object.
Takeaway: Define a single, composable capability interface from day one. Don't have "special" capabilities that bypass the abstraction.
Practice: The query loop is an AsyncGenerator<Message | StreamEvent>. Tool results are yielded incrementally, not batched. UI updates in real-time.
Why it works: Users see progress immediately. Long-running tools don't block the UI. Network latency is hidden by streaming partial results.
Takeaway: Build streaming into your core types, not as an afterthought. AsyncGenerator is the right primitive for agent loops.
Practice: Every query tracks token usage. Auto-compaction triggers before the context limit. The model can "continue" when it hits max_tokens via a synthetic tool call.
Why it works: Long agentic tasks don't silently fail mid-way. Users running 50-turn sessions have their context managed transparently.
Diagram:
graph LR
TRACK[Track tokens per turn] --> CHECK{Over budget?}
CHECK -->|Yes| COMPACT[Compact messages\n- tombstone old user msgs\n- summarize assistant turns]
CHECK -->|No| PROCEED[Proceed normally]
COMPACT --> PROCEED
PROCEED --> MAX{stop_reason\n= max_tokens?}
MAX -->|Yes| CONTINUE[Auto-continue\nvia continuation tool]
CONTINUE --> TRACK
MAX -->|No| DONE[End turn]
Takeaway: Token management is not optional for production agents. Build compaction and continuation into the loop core.
Practice: Permissions are checked at five layers (static rules → tool logic → hooks → dialog → ML classifier), with each layer able to short-circuit.
Why it works: Power users can set alwaysAllow patterns for productivity. Cautious users get prompted for every action. Enterprise deployments use deny rules for compliance. All on the same code path.
Takeaway: Ship with deny-by-default permissions and multiple layers of override, not a single boolean "trust" flag.
Practice: Non-critical tools are marked shouldDefer: true. They don't appear in the model's initial context — the model must call ToolSearch to discover and load them.
Why it works: Keeps the initial prompt lean (fewer tokens). Model only loads tools it actually needs. Reduces cognitive overhead in tool selection.
Takeaway: Don't give the model 100 tools upfront. Use lazy discovery for rarely-needed capabilities.
Practice: Capabilities are wrapped in feature('flagName', () => import('./module')). The bundler eliminates disabled features at build time.
Why it works: Different build targets (OSS CLI, IDE extension, enterprise) can enable/disable capabilities without runtime overhead. No dead code ships.
Takeaway: Use feature flags + build-time tree shaking for capability sets, not runtime if-else chains.
Practice: Key lifecycle events emit hooks (PreToolUse, PostToolUse, UserPromptSubmit, etc.) that external scripts can handle.
Why it works: Custom permission policies, audit logging, input sanitization, and output transformation don't require forking the codebase. Power users automate their workflows.
Takeaway: Design extensibility as observable events + transformation points, not plugin APIs that need versioning.
Practice: AppState is typed as DeepImmutable<T>. Updates go through a controlled store.update(fn) function that creates a new state snapshot.
Why it works: In a streaming async environment with concurrent tool calls, mutable shared state is a landmine. Immutability makes state transitions auditable.
Takeaway: In an agentic system, deep immutability prevents entire classes of async race condition bugs.
Practice: The 15-phase startup in init.ts has an explicit order: early side effects → config → MDM → trust dialog → environment → API preconnect → feature flags → MCP.
Why it works: Trust-sensitive operations (reading env vars, connecting to services) don't run until the user has consented. Security posture is established before capability.
Diagram:
graph LR
P1[Profiling\n& MDM read] --> P2[Config\nvalidation]
P2 --> P3[Safe env vars\nbefore trust]
P3 --> P4[Shutdown\nhandlers]
P4 --> P5[MTLS\n& proxy]
P5 --> P6[OAuth\npopulate]
P6 --> P7[Policy\nlimits]
P7 --> P8[Remote settings\nfetch]
P8 --> P9[Trust dialog\nif first run]
P9 --> P10[Full env vars\nafter trust]
P10 --> P11[API\npreconnect]
P11 --> P12[GrowthBook\nfeature flags]
P12 --> P13[Bootstrap\ndata fetch]
P13 --> P14[MCP server\nconnect]
P14 --> P15[Session\nrecovery]
P15 --> READY[Ready]
Takeaway: Enumerate your startup phases explicitly. Put trust-sensitive initialization after consent, not before.
Practice: Before every API call, the system auto-injects: memory files (.claude.md), file change events, skill discovery, and tool prefetches as attachment messages.
Why it works: The model always has up-to-date context without the user having to re-explain the codebase, open files, or recent changes.
Takeaway: Invest heavily in automatic context enrichment. The model's quality is bounded by the quality of its context.
Practice: While the user is typing, Claude Code pre-runs background agent guesses at what the next intent might be (speculation / pipelining).
Why it works: Responses feel instant because work was done before the user hit Enter. The timeSavedMs metric measures actual impact.
Takeaway: Precompute probable next states. Agents feel fast not just through streaming, but through predicting what comes next.
Claude Code treats safety as a primary engineering concern, not an afterthought:
- Multiple permission layers (rules, hooks, dialogs, ML classifier)
alwaysDenyRulesfor catastrophically dangerous patterns (rm -rf /)- Trust dialog at first run (no side effects before consent)
- Sub-agent isolation via git worktrees
- Immutable state to prevent race conditions
Key insight: Safety mechanisms are composable. A user can configure alwaysAllow for their workflow without weakening safety for others.
The MCP + Hook + Plugin architecture means the community can extend Claude Code without touching Anthropic's code:
graph TB
CORE[Claude Code Core\nAnthropic maintained]
subgraph "Community Extensions"
MCP_EXT[MCP Servers\next tools & resources]
HOOK_EXT[Hook Scripts\ncustom policy & logic]
SKILL_EXT[Skills\ncustom workflows]
PLUGIN_EXT[Plugins\ncustom commands & UI]
end
CORE -->|event bus| HOOK_EXT
CORE -->|protocol| MCP_EXT
CORE -->|discovery| SKILL_EXT
CORE -->|API| PLUGIN_EXT
Key insight: The extensibility surface area is small and stable (tool interface, hook events, MCP protocol). This is what makes the ecosystem grow.
The system invests enormously in context quality:
- Automatic memory file injection (
.claude.mdat project + user level) - File change tracking and injection
- Skill discovery auto-attached to prompts
- Speculative prefetch of likely-needed tools
- Context compaction to maximize usable window
- Prompt caching for repeated content
Key insight: For a coding agent, 80% of quality comes from "does the model have the right context?" not "is the model smart enough?".
Claude Code degrades gracefully at every layer:
- Rate limits: Automatic fallback to cheaper/smaller models
- Max tokens: Auto-continuation via synthetic tool call
- Context overflow: Intelligent message compaction
- MCP server failure: Continues with remaining tools
- Tool failure: Error surfaced as
tool_result, not crash - Network failure: Retry with exponential backoff
Key insight: Agent systems interact with the real world. Plan for every external dependency to fail.
- OpenTelemetry integration for distributed tracing
- Startup profiler tracks initialization timing
- Token usage reported per-turn in verbose mode
- Task output disk-streamed for forensics
/doctorcommand for health diagnostics- Analytics events for every tool call, approval, and compaction
Key insight: You cannot improve an agent you cannot observe. Ship telemetry before shipping features.
Claude Code is built with Ink (React for terminals) and has 87+ custom React hooks for terminal UX:
- Vim keybinding mode
- Arrow-key command history
- Real-time streaming with diff display
- Progress spinners for background tasks
- Interactive permission dialogs
/compactview for long sessions
Key insight: For a CLI agent, terminal UX is product quality. Users who get frustrated abandon the tool regardless of AI capability.
graph TB
subgraph "Language & Runtime"
TS[TypeScript\nstatic typing + generics]
BUN[Bun\nbundler + runtime]
TSX[TSX / React\nfor Ink UI]
end
subgraph "AI & Protocol"
SDK[Anthropic SDK\n@anthropic-ai/sdk]
MCP_SDK[MCP SDK\n@modelcontextprotocol/sdk]
OTEL_SDK[OpenTelemetry SDK]
end
subgraph "UI"
INK[Ink\nReact for terminals]
REACT[React 18\nuseS yncExternalStore]
end
subgraph "Schema & Validation"
ZOD[Zod v4\nruntime validation]
ZOD_LAZY[Zod lazy schemas\ncircular dependency]
end
subgraph "Utilities"
LODASH[lodash-es\nmemoize, debounce, etc.]
GLOB[glob\nfile pattern matching]
DIFF[diff\npatch generation]
end
subgraph "External Services"
ANTHROPIC[Anthropic API]
GB_SVC[GrowthBook\nfeature flags]
OAUTH_SVC[OAuth providers]
BEDROCK[AWS Bedrock]
VERTEX[GCP Vertex AI]
end
TS --> BUN
TSX --> INK
SDK --> ANTHROPIC
SDK --> BEDROCK
SDK --> VERTEX
ZOD --> ZOD_LAZY
Why Bun? Faster startup, built-in bundler with dead code elimination via feature flags, TypeScript support without transpile step.
Why Ink? React's component model maps naturally to a streaming terminal UI — components re-render on state changes, just like in a browser.
Why Zod v4? Schema-based validation for tool inputs prevents entire classes of runtime errors. z.lazy() solves circular schema dependencies in recursive tool definitions.
| Dimension | Chatbot Wrapper | Claude Code |
|---|---|---|
| Tool calling | Optional, demo-quality | Core abstraction, 43+ tools, concurrent |
| Context management | Manual, user-driven | Automatic injection, compaction, caching |
| Safety | Single approve/deny | 5-layer permission system |
| Extensibility | Fork the code | MCP + Hooks + Plugins |
| Multi-agent | Single thread | Worktrees, threads, remote agents |
| State management | Mutable object | DeepImmutable store |
| Streaming | Optional bolt-on | AsyncGenerator core type |
| Observability | Logs | OpenTelemetry + startup profiler + analytics |
| Startup | Eager init | 15-phase sequenced pipeline |
| UX | Raw terminal output | Ink React components, vim mode, history |
-
Make Tool the core abstraction. Everything the agent can do is a Tool. No special cases.
-
Stream everything. Use
AsyncGeneratorfrom the protocol layer to the UI. Users tolerate slow; they don't tolerate silent. -
Manage context proactively. Auto-inject memory, track file changes, compact old messages. The model's quality is bounded by context quality.
-
Build safety in layers. Static rules → tool logic → hooks → user dialog → ML classifier. Each layer composable and independently configurable.
-
Defer non-critical tools. Keep the initial tool list small. Use lazy discovery (
ToolSearch) for the long tail. -
Make state immutable.
DeepImmutable<AppState>+ controlled update functions. Async agent systems with mutable state will corrupt state eventually. -
Sequence your startup. Trust/consent before capability. Security posture before feature availability.
-
Design for graceful degradation. Every external dependency will fail. Plan for rate limits, network errors, context overflow, model refusals.
-
Build extensibility via events, not APIs. Hook events + MCP protocol scales better than plugin APIs with versioning.
-
Ship telemetry first. You cannot improve what you cannot observe. OpenTelemetry, startup profiling, and per-turn analytics are not optional.
mindmap
root((Claude Code\nArchitecture))
Core Loop
AsyncGenerator query
Streaming turns
Token budgeting
Auto-compaction
Auto-continuation
Tool System
Unified Tool interface
Lazy / deferred loading
Concurrent execution
Schema validation
Result rendering
Safety
5-layer permissions
alwaysDeny rules
PreToolUse hooks
ML classifier
Worktree isolation
Context Quality
Memory injection
File change tracking
Skill discovery
Prompt caching
Speculative prefetch
Extensibility
MCP protocol
Hook events
Plugin system
Skills
Commands
State
DeepImmutable store
useSyncExternalStore
Task registry
Agent registry
UX
Ink React terminal
87+ React hooks
Vim keybindings
Real-time streaming
Permission dialogs
Observability
OpenTelemetry
Startup profiler
Token reporting
Analytics events
/doctor command
Report generated from source analysis of /Users/liuyong/code/cc-src.
Technology stack confirmed: TypeScript, Bun, React/Ink, Anthropic SDK, MCP SDK, Zod v4.