外洩的 Claude Code 全量原始碼,終於讓我們有機會看清楚:一個日活百萬的 AI Coding Agent,在工程層面到底做了哪些事。這篇文章從啟動流程、Agent 迴圈、工具系統、權限模型、上下文管理、多 Agent 協作等維度逐一拆解。
Claude Code 並不是一個簡單的 "LLM + 命令列包裝"。它是一個用 TypeScript + React (Ink) 構建的、運行在終端機裡的完整 Agent 作業系統。
幾個關鍵判斷:
bun:bundle 做死碼消除),同時部分路徑相容 Node(例如容器環境),UI 用 React Ink,API 用 Anthropic SDK,整體約 60+ 內建工具、60+ 使用者指令,程式碼量非常大。此外還用純 TypeScript 重寫了三個原本的 native 依賴(語法高亮 diff、模糊檔案搜尋、yoga flexbox 佈局),顯著縮小了 NAPI 依賴面——但並未徹底消除,倉庫中仍有 audio-capture-napi(語音錄製)、url-handler-napi(macOS URL scheme)、image-processor-napi(圖片處理/剪貼簿)、modifiers-napi(鍵盤修飾鍵偵測)等 NAPI 模組在用。如果你在打造 Coding Agent,這份原始碼是最好的參考實作之一。
看 main.tsx 的前 20 行就知道,Anthropic 在啟動效能上下了很大功夫:
// 這三個 side-effect 必須在所有其他 import 之前運行:
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead(); // 並行:MDM 配置子程序
import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch(); // 並行:macOS 鑰匙圈預讀(OAuth + API key)
這裡的設計思路是:把昂貴的 I/O 操作(鑰匙圈讀取、MDM 配置讀取)提前到 import 階段並行執行。因為 Node/Bun 的 import 本身需要大約 135ms 來載入模組,這段時間正好可以用來做非同步預熱。
整個互動式啟動序列大致如下:
(注意:-p / --print 非互動模式下不會彈出 Trust Dialog——原始碼註解明確寫了 "trust is implicit in -p mode"。)
關鍵點:
feature('COORDINATOR_MODE') 來自 bun:bundle,是構建期的死碼消除,編譯後不存在;GrowthBook/Statsig 是運行期遠端配置,走 getFeatureValue_CACHED_MAY_BE_STALE() / checkStatsigFeatureGate_CACHED_MAY_BE_STALE() 的程式碼路徑。require() 延遲載入來打破循環依賴。Claude Code 的核心迴圈有兩條路徑:
一個容易誤解的點:REPL 並不經過 QueryEngine。QueryEngine 的註解明確寫了 "供 headless/SDK 使用,REPL 是 future phase"。從 screens/REPL.tsx 可以看到,REPL 直接透過 for await (const event of query({...})) 呼叫 query()。
QueryEngine 是 headless/SDK 模式下每個對話的狀態持有者,負責:
export class QueryEngine {
private mutableMessages: Message[] // 訊息歷史
private abortController: AbortController // 中斷控制
private permissionDenials: SDKPermissionDenial[] // 權限拒絕記錄
private totalUsage: NonNullableUsage // 累計 token 用量
private readFileState: FileStateCache // 檔案狀態快取
private discoveredSkillNames = new Set<string>() // 技能發現追蹤
}
它的核心方法是 submitMessage(),每次使用者送出訊息時被呼叫,觸發一輪完整的 Agent 迴圈。
query() 函數是真正的執行引擎,實作了一個 generator 模式的 Agent 迴圈。主要工程亮點如下。
幾個工程亮點:
Claude Code 不是等 LLM 完整輸出後再執行工具,而是在流式回應的過程中就開始準備工具呼叫:
import { StreamingToolExecutor } from './services/tools/StreamingToolExecutor.js';
這裡有一個很容易忽略的點:原始碼中有兩套並行執行實作,分別用於不同場景。
舊路徑:toolOrchestration.ts(批量分區)
function partitionToolCalls(toolUseMessages, toolUseContext): Batch[] {
// 對每個工具呼叫,根據 tool.isConcurrencySafe(parsedInput) 判定:
// 1. 連續的 concurrency-safe 工具 → 並行執行(最多 10 並發)
// 2. 單個非 concurrency-safe 工具 → 串行執行
}
新路徑:StreamingToolExecutor(流式並發)
當 streamingToolExecution gate 開啟時,工具不再等 LLM 一次性返回所有 tool_use 再分區,而是一邊流式接收 tool_use 塊、一邊立即開始執行:
class StreamingToolExecutor {
addTool(block: ToolUseBlock, assistantMessage: AssistantMessage) {
// 流式接收到一個 tool_use 就立即入隊
// 如果目前沒有非並發安全的工具在執行,立即啟動
}
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = this.tools.filter(t => t.status === 'executing')
return executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
}
}
還有一個巧妙的錯誤傳播機制:當某個並發的 Bash 工具出錯時,StreamingToolExecutor 會透過 siblingAbortController(父 AbortController 的子控制器)立即取消兄弟進程,而不會中斷父 query 迴圈。
注意判定依據不是簡單的「只讀 vs 寫入」,而是每個工具自己實作的 isConcurrencySafe(input) 方法,會根據具體輸入參數判斷。例如 FileReadTool 通常返回 true,但 BashTool 會根據指令內容決定。
當對話的 token 數接近上下文視窗時,系統會自動觸發壓縮:
// 閾值 = 有效上下文視窗 - 13000 tokens 緩衝
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
// 連續失敗超過 3 次就停止重試(避免浪費 API 呼叫)
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
但 auto compact 只是壓縮策略之一。原始碼中至少還有這些路徑:
prompt_too_long 錯誤時被動觸發的緊急壓縮。HISTORY_SNIP feature flag 時參與請求前壓縮。CONTEXT_COLLAPSE)。這些路徑不是互斥的——snip 和 microcompact 可以同時執行,autocompact 在它們之後觸發。
從 query.ts 的迴圈體可以看到完整的壓縮流水線執行順序:
每一層都可能縮減上下文,後面的層看到的是前面處理過的結果。如果 snip + microcompact + collapse 已經把 token 數降到閾值以下,autocompact 就不用觸發了——這比單純的 autocompact 保留了更多細粒度上下文。
當 Claude 的回覆被截斷(達到 max_output_tokens 限制)時:
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3
// 最多重試 3 次,每次自動 continue
tools.ts 是內建工具(built-in tools)的註冊中心。MCP 工具(MCPTool、McpAuthTool)不在這裡註冊,而是在 MCP client 層(services/mcp/client.ts)動態建立,最後由 assembleToolPool() 合併進統一的工具池。
從原始碼可以看到 built-in 工具列表(不含 MCP 動態工具):
工具註冊大量使用了 Bun 的 feature() 做構建期的死碼消除:
const WorkflowTool = feature('WORKFLOW_SCRIPTS')
? (() => {
require('./tools/WorkflowTool/bundled/index.js').initBundledWorkflows()
return require('./tools/WorkflowTool/WorkflowTool.js').WorkflowTool
})()
: null
const MonitorTool = feature('MONITOR_TOOL')
? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
: null
當某個 feature flag 關閉時,對應的 require() 不會執行,相關程式碼會被 Bun 的 bundler 徹底剔除。這不是執行期檢查,而是構建期優化。
在工具到達 LLM 之前,會經過 deny rules 過濾:
export function filterToolsByDenyRules(tools, permissionContext) {
return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
}
被 deny 的工具連 schema 都不會發送給模型——模型根本不知道這些工具的存在。
這是 Claude Code 最值得學習的部分之一。真實的權限檢查流程比「四道關」複雜得多,大致如下:
(流程圖略述:deny rules → hooks/classifier → ask rules → tool 自檢 → permission mode → 使用者確認 / classifier 並發競速 → 實際執行)
一個重要細節:hooks/classifier 不是在使用者互動之後串行執行的,而是和使用者確認對話框並發競速——哪個先返回結果就用哪個。
原始碼中的模式比「三種」多得多:
// types/permissions.ts
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits', // 自動接受編輯操作
'bypassPermissions', // 旁路,所有操作自動通過
'default', // 預設,敏感操作需確認
'dontAsk', // 不彈確認框(背景 agent 用)
'plan', // 計畫模式,只讀自動通過
] as const
// 內部還有:
export type InternalPermissionMode =
| ExternalPermissionMode
| 'auto' // feature flag 開啟時的自動模式
| 'bubble' // 權限冒泡(子 agent → 父 agent)
BashTool 的權限檢查特別複雜,單獨有一個 bashSecurity.ts:
parseForSecurity() 解析 bash 指令的 AST,而不是簡單的字串比對。sedEditParser.ts 來判斷 sed 指令是否在做檔案編輯。destructiveCommandWarning.ts 偵測 rm -rf 等危險指令。shouldUseSandbox() 判斷是否需要在 sandbox 中執行。pathValidation.ts 確保不會操作專案外的檔案。原始碼中有 TRANSCRIPT_CLASSIFIER feature flag:
const classifierDecisionModule = feature('TRANSCRIPT_CLASSIFIER')
? require('./classifierDecision.js')
: null
這意味著 Anthropic 在權限判斷中還引入了一個分類器模型,來輔助判斷工具呼叫是否安全。
Task.ts 定義了任務的類型系統:
export type TaskType =
| 'local_bash' // 本地 Shell 指令
| 'local_agent' // 本地子 Agent
| 'remote_agent' // 遠端 Agent
| 'in_process_teammate'// 進程內隊友(Agent Swarms)
| 'local_workflow' // 本地工作流
| 'monitor_mcp' // MCP 監控
| 'dream' // "做夢"(背景整合)
export type TaskStatus =
| 'pending' | 'running' | 'completed' | 'failed' | 'killed'
每個任務都有一個密碼學安全的 ID:
// 前綴 + 8 字節隨機 = 36^8 ≈ 2.8 兆種組合
// 足以抵禦符號連結攻擊
export function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type) // b/a/r/t/w/m/d
const bytes = randomBytes(8)
// ...
}
當 CLAUDE_CODE_COORDINATOR_MODE=1 時,Claude Code 進入協調者模式:
export function isCoordinatorMode(): boolean {
if (feature('COORDINATOR_MODE')) {
return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
}
return false
}
在這個模式下:
SendMessageTool 在 Agent 間傳遞訊息。// constants/tools.ts
export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
AGENT_TOOL_NAME,
TASK_STOP_TOOL_NAME,
SEND_MESSAGE_TOOL_NAME,
SYNTHETIC_OUTPUT_TOOL_NAME,
])
// 原始碼中的團隊管理工具
TeamCreateTool // 建立團隊成員
TeamDeleteTool // 刪除團隊成員
SendMessageTool // 向團隊成員發訊息
ListPeersTool // 列出所有 Agent 同伴
這就是 Anthropic 的「多 Agent 協作」實作——不是透過外部編排框架,而是直接內建在工具系統裡。
runAgent.ts 揭示了子 Agent 的運作方式:
FileStateCache(檔案狀態隔離)。query() 迴圈。子 Agent 有自己的工具限制(CUSTOM_AGENT_DISALLOWED_TOOLS)。外部建構中,AgentTool 預設在禁止名單裡,防止無限遞歸;但 Anthropic 內部建構(USER_TYPE === 'ant')允許嵌套 agent。此外,in-process teammate 在特定條件下也能取得 AgentTool。
除了上面透過 AgentTool 顯式建立的子 Agent,原始碼中還有一種 Fork Subagent 模式(FORK_SUBAGENT feature flag):
'bubble',權限請求會上浮到父 agent 由使用者確認。isInForkChild() 偵測並阻止巢狀 fork。這個設計非常聰明——Fork Subagent 本質上是「用當前對話上下文開一個分支去做某件事」,而不是 AgentTool 那種「建立一個新 agent 從零開始」。適用於「幫我同時驗證這三個檔案」這類場景。
context.ts 只負責產出 systemContext / userContext 兩個片段——前者主要是 git 狀態快照,後者主要是 CLAUDE.md 內容。
// context.ts → getSystemContext() 收集:
const [branch, mainBranch, status, log, userName] = await Promise.all([
getBranch(),
getDefaultBranch(),
execFileNoThrow(gitExe(), ['status', '--short']),
execFileNoThrow(gitExe(), ['log', '--oneline', '-n', '5']),
execFileNoThrow(gitExe(), ['config', 'user.name']),
])
// → 打包成 git 狀態快照
真正的 System Prompt 組裝分散在多個檔案中:
getSystemPrompt() ← constants/prompts.ts:基礎提示詞模板fetchSystemPromptParts() ← utils/queryContext.ts:協調 systemPrompt + userContext + systemContextbuildEffectiveSystemPrompt() ← REPL 層的最終組裝最終送給模型的 request payload 包括:
Claude Code 的 CLAUDE.md 不只是「專案根目錄的一個檔案」,而是個多層級配置系統。claudemd.ts 開頭的註解寫得很清楚:
載入順序(從低優先級到高優先級):
還支援 @include 指令引用其他檔案,形成組合配置。
這是本文首次發現時容易低估的部分。Claude Code 的記憶不是一個簡單的 MEMORY.md 檔案——從程式碼架構來看,可以歸納為一個三層漸進式記憶管線(以下為作者分析框架,非官方分層)。需要注意的是,這三層都帶有獨立的 gate 或運行條件,不是無條件常駐能力:Auto Memory 要檢查 isAutoMemoryEnabled() 且 remote 模式會跳過;Session Memory 由 tengu_session_memory feature flag 控制;Auto Dream 在 KAIROS 和 remote 模式下直接關閉。
第一層:Auto Memory Extraction
每輪 query 結束後(不再有 tool_use 時),背景 forked agent 自動提取對話中的關鍵洞察,寫入專案記憶目錄。
runForkedAgent() + Prompt Cache 共用(和主對話共用 cache prefix,避免雙倍 API 成本)。sinceUuid):如果主 agent 自己已經寫了記憶,forked agent 會跳過,避免重複。第二層:Session Memory
當對話足夠長時(三重閾值:10K tokens 初始化 + 5K tokens 增量 + 3 次工具呼叫),自動提取 session 級筆記。
這個 session memory 還有一個巧妙用途:Session Memory Compaction 可以用 session memory 的內容來替代 autocompact 的摘要——因為 session memory 是增量提取的,品質可能比一次性摘要更好。
第三層:Auto Dream(記憶整合)
這是最有趣的設計。當滿足兩個條件:
Claude Code 會啟動一個 "dream" 任務(TaskType = 'dream'),用 forked agent 遍歷多個 session 的 transcript,把分散的記憶整合沈澱到專案記憶中。
const DEFAULTS: AutoDreamConfig = {
minHours: 24,
minSessions: 5,
}
Dream 任務有完整的生命週期管理(register → phase tracking → complete/fail),可以被 kill 並回滾 consolidation lock 的 mtime。這解釋了 Task.ts 中那個神祕的 'dream' 類型。
MEMORY.md 入口檔
所有記憶內容透過 MEMORY.md 入口檔注入 system prompt:
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
有行數和位元數雙重截斷保護——因為使用者可能寫出單行超長的 MEMORY.md(實際觀測到 197KB 在 200 行以下的情況)。
Claude Code 對 MCP(Model Context Protocol)的支援非常深入:
// 支援所有 MCP 傳輸方式
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
prefetchAllMcpResources() 預載 MCP 資源。assembleToolPool() 中統一合併,內建工具優先。prefetchOfficialMcpUrls() 從官方註冊表取得 MCP 伺服器列表。bridge/ 目錄揭示了 Claude Code 的遠端執行能力:
這套架構允許 Claude Code 在以下情境下工作:
Claude Code 不只是個封閉的工具集——它已經構建了一套完整的擴展體系。
插件安裝管理器(PluginInstallationManager.ts)實作了一個完整的 diff-reconcile 流程:先算出 missing/updated/up-to-date/failed 的差異,再非同步並行安裝,帶進度回調到 UI。
Skill 是比 Plugin 更輕量的擴展單元——本質上是帶 YAML frontmatter 的 Markdown 檔:
@include 引用、argument 取代、shell 執行指令EXPERIMENTAL_SKILL_SEARCH gate 下,每輪對話會預取可能相關的 skillutils/hooks/ 下有一套獨立的執行生命週期 hook 系統:
Hook 類型說明
Hook 不只是 shell command——原始碼定義了四種 hook 類型:command(shell 指令)、prompt(LLM prompt)、agent(多輪 Agent 查詢)、http(HTTP POST)。各類型的預設逾時也不同:shell/tool hook 預設 10 分鐘,prompt hook 預設 30 秒,agent hook 預設 60 秒,http hook 預設 10 分鐘,只有 AsyncHookRegistry 的 asyncTimeout 預設 15 秒。所有 hook 支援進度增量輸出(stdout/stderr delta 即時推送 UI)。
Claude Code 用 Ink(React for CLI)構建終端機 UI。以下是高層示意的元件結構(非實際渲染樹):
幾個有趣的點:
native-ts/ 目錄包含三個原本是 native NAPI 模組的純 TypeScript 重寫:
color-diff/:語法高亮 diff(替代 Rust syntect,改用 highlight.js)file-index/:模糊檔案搜尋(替代 Rust nucleo,非同步建構 index,每 4ms yield 事件迴圈)yoga-layout/:Flexbox 佈局引擎(替代 Meta 的 C++ yoga-layout,single-pass 實作 Ink 所需的子集)文章初版只提了 BashTool 安全的概要。深入 bashSecurity.ts 後發現,原始碼定義了 23 個獨立的安全檢查 ID(BASH_SECURITY_CHECK_IDS),程式碼中多處提到 "defense-in-depth" 設計理念,但並沒有官方分層命名。以下是我將這些檢查點歸納為 8 層的分析框架:
層級名稱與功能摘要:
'x'# 等邊緣情況)。$()、進程替換 <(...)>、zsh equals expansion =curl、heredoc in sub 等。zmodload(通往 mapfile/zpty/ztcp)、emulate -c(等同 eval)、zf_rm 等繞過檢查的 builtin。-- 終止符。/proc/self/environ 存取。rm -rf、git reset --hard、DROP TABLE、kubectl delete、terraform destroy 等(展示警告但不直接阻止)。sed -i 為檔案編輯操作(BRE↔ERE 轉換用 null-byte sentinel 防注入),處理 macOS -i 後綴差異。一個特別有趣的安全細節:zsh 的 =curl 語法會展開為 /usr/bin/curl,這意味著如果你在 deny rules 裡禁了 Bash(curl:*),攻擊者可以透過 =curl 繞過。Claude Code 在第 2 層專門阻止了這種 zsh equals expansion。
從 feature flag 和程式碼結構推測,Claude Code 的程式庫可能支援多種產品形態(以下為從程式碼結構推測的分類,並非原始碼的官方命名):
| 形態 | Feature Flag | 說明 |
|---|---|---|
| CLI | 預設建置 | 我們使用的命令列版本 |
| KAIROS / Assistant | feature('KAIROS') | 類似 assistant 模式,有 PushNotification、SendUserFile、SubscribePR、session chooser、install wizard |
| Coordinator | feature('COORDINATOR_MODE') | 多 Agent 協調者模式 |
| Bridge / Remote | bridge/ + remote/ 目錄 | 遠端會話橋接(程式碼中出現 "CCR" 縮寫,但具體產品定位不明) |
除了 bridge/,還有兩個遠端執行路徑:
remote/:RemoteSessionManager + SessionsWebSocket,透過 SDK control messages 實現遠端權限橋。server/:DirectConnectSessionManager,WebSocket 直連模式,Bun 原生 WebSocket headers 支援。這意味著 Anthropic 在用一套程式庫同時開發本地 CLI、遠端託管、以及可能的 SaaS assistant 產品。moreright/ 目錄只有一個 useMoreRight.tsx stub 檔(註解寫明 "Stub for external builds — the real hook is internal only"),其中 onBeforeQuery 返回 true,onTurnComplete 是空 no-op,只有 render 返回 null——真實實作應該在 Anthropic 內部建置中。
這是整個專案最獨特的模式。透過 Bun 的 feature() 函數:
import { feature } from 'bun:bundle'
const coordinatorModeModule = feature('COORDINATOR_MODE')
? require('./coordinator/coordinatorMode.js')
: null
這不是運行期的 if-else——Bun bundler 在構建時會根據 feature flag 設定,把關閉的分支整個刪掉。這意味著不同的建置產物可以有完全不同的功能集。
但同時還有一套運行期遠端配置,透過 GrowthBook/Statsig 下發:
// 運行期檢查,走遠端配置
getFeatureValue_CACHED_MAY_BE_STALE('some_feature')
checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_scratch')
這兩套系統服務於不同目的:構建期消除維度是「產品形態」(CLI vs KAIROS vs Coordinator),運行期配置維度是「灰度發佈」和「A/B 測試」。
專案中大量使用 require() 延遲載入:
// Lazy require to avoid circular dependency
const getTeammateUtils = () =>
require('./utils/teammate.js') as typeof import('./utils/teammate.js')
這是大型 TypeScript 專案常見的痛點解法。
import memoize from 'lodash-es/memoize.js'
export const getGitStatus = memoize(async () => { ... })
系統上下文、git 狀態等昂貴操作都被 memoize,避免同一次對話中重複計算。
process.env.USER_TYPE === 'ant' // Anthropic 內部員工
內部員工有額外的工具(REPLTool、ConfigTool、TungstenTool、SuggestBackgroundPRTool),外部使用者看不到。
這是專案中最被低估的工程模式之一。Auto Memory Extraction、Agent Summary、Magic Docs、Prompt Suggestion、Auto Dream 等背景功能都基於同一個模式:
// runForkedAgent() + CacheSafeParams
// 關鍵:fork 出的 agent 和主對話共享 system prompt 前綴
// → 命中 prompt cache → fork 的 API 成本極低
為了保證 cache 命中,fork agent 甚至會特意保持 system prompt 位元完全一致——GrowthBook 冷/熱啟動時可能返回不同值,所以渲染好的 system prompt 透過 toolUseContext.renderedSystemPrompt 顯式傳遞,而不是讓 fork child 自己重新渲染。
全域狀態管理的核心是 state/store.ts——只有 36 行程式碼:
export function createStore<T>(initialState: T, onChange?: OnChange<T>): Store<T> {
let state = initialState
const listeners = new Set<Listener>()
return {
getState: () => state,
setState: (updater) => { /* ... */ },
subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener) },
}
}
透過 React 的 useSyncExternalStore 整合到 Ink 元件樹。沒有 Redux、Zustand 或任何狀態管理庫。證明了對於這種確定性狀態流的應用,最簡單的 pub-sub store 就夠了。
bun:bundle 的 feature(),意味著與 Bun 強綁定。讀完這份原始碼,我的判斷是:
Claude Code 不是一個 demo 級產品,它是一個經過深度工程化的工業級 Agent 系統。從啟動的並行初始化優化到工具並發調度,從多層權限模型到構建期死碼消除,每一個細節都透露著「這是一個真實服務百萬使用者的產品」。
幾個值得 Agent 開發者學習的設計:
這可能是目前市面上最值得深入學習的 Coding Agent 參考實作。