💥 Claude Code 源码泄露?我把这个最强 AI Coding Agent 的架构扒干净了

1 阅读23分钟

2026 年 3 月 31 日下午,一条推特引爆了整个技术圈——@Fried_rice 曝光了 Claude Code 的完整源码泄露事件。消息一出,开发者社区瞬间沸腾。

image.png

说实话,作为一个长期关注 AI Agent 架构的工程师,看到这条消息的第一反应不是吃瓜,而是——终于可以验证我对 Claude Code 的猜想了。之前用它写代码时,一直好奇它的"手感"为什么和其他 AI 编程工具完全不一样:上下文似乎永远不会丢、工具调用快得不像是串行的、权限管控粒度细到单条命令。既然源码有了,就了解下吧~

一、Claude Code 到底是什么?

如果你只把 Claude Code 当成"命令行版的 Copilot",那就严重低估它了。

从源码角度看,Claude Code 是一个完整的 Agent 运行时系统——它有自己的 Agent 主循环、工具注册与调度框架、多层上下文管理策略、权限控制体系、插件与技能扩展机制、子 Agent 编排能力,甚至还有一套基于 Ink(React for Terminal)的完整终端 UI 框架。

它和 ChatGPT、GitHub Copilot 的本质区别在于:

维度ChatGPT / CopilotClaude Code
交互模式对话 / 补全自主 Agent 循环
工具能力无 / 有限47+ 内置工具,支持 MCP 扩展
上下文管理简单截断5 层上下文压缩管道
代码修改输出代码片段直接编辑文件系统
权限控制3 层权限架构 + AST 级命令分析
子任务多 Agent 并发编排

Claude Code 值得研究的原因很简单:它是目前工程化程度最高的 AI Coding Agent 实现之一,其架构设计中蕴含了大量可复用的工程模式。

二、整体架构拆解

在深入源码之前,先看 Claude Code 的全局架构。我画了一个分层视图:

┌─────────────────────────────────────────────────────────────────┐
│                     用户交互层 (Ink/React Terminal UI)            │
│  ┌──────────┐ ┌────────────┐ ┌──────────┐ ┌──────────────────┐  │
│  │PromptInput│ │MessageList │ │ Spinner  │ │PermissionDialog  │  │
│  └──────────┘ └────────────┘ └──────────┘ └──────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                   REPL 编排层 (screens/REPL.tsx)                  │
│     ┌──────────────────────────────────────────────────┐        │
│     │  useReplBridge · useCanUseTool · useTypeahead    │        │
│     └──────────────────────────────────────────────────┘        │
├─────────────────────────────────────────────────────────────────┤
│                   Agent 核心层                                    │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────────┐     │
│  │ query()      │  │ QueryEngine  │  │ SubAgent 编排      │     │
│  │ Agent 主循环  │  │ 会话编排器    │  │ (AgentTool)       │     │
│  └──────┬───────┘  └──────────────┘  └───────────────────┘     │
│         │                                                       │
│  ┌──────▼───────────────────────────────────────────────┐      │
│  │              工具调度层                                │      │
│  │  toolOrchestration → toolExecution                   │      │
│  │  StreamingToolExecutor (流式并行执行)                   │      │
│  └──────────────────────────────────────────────────────┘      │
├─────────────────────────────────────────────────────────────────┤
│                   工具实现层 (47+ Tools)                          │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐       │
│  │ Bash │ │ Edit │ │ Read │ │ Grep │ │ Agent│ │ Skill│ ...   │
│  └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘       │
├─────────────────────────────────────────────────────────────────┤
│                   上下文管理层                                    │
│  ToolResultBudget → SnipCompact → MicroCompact                 │
│      → ContextCollapse → AutoCompact → ReactiveCompact         │
├─────────────────────────────────────────────────────────────────┤
│                   基础设施层                                      │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────────┐   │
│  │State   │ │Perms   │ │Memory  │ │Plugins │ │MCP/Bridge  │   │
│  │Store   │ │System  │ │System  │ │/Skills │ │Integration │   │
│  └────────┘ └────────┘ └────────┘ └────────┘ └────────────┘   │
└─────────────────────────────────────────────────────────────────┘

下面逐层拆解。

三、Agent 主循环:一个精密的异步状态机

这是 Claude Code 最核心的代码,位于 src/query.ts

3.1 整体结构

整个 Agent 循环被实现为一个异步生成器(async generator),核心就是一个 while(true) 循环:

// src/query.ts
export async function* query(params: QueryParams): AsyncGenerator<
  StreamEvent | RequestStartEvent | Message | TombstoneMessage | ToolUseSummaryMessage,
  Terminal
> {
  const consumedCommandUuids: string[] = []
  const terminal = yield* queryLoop(params, consumedCommandUuids)
  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed')
  }
  return terminal
}

query() 是一个薄壳,实际逻辑在 queryLoop() 里。注意返回类型 Terminal——这是一个离散枚举,表示循环退出的原因(completed、aborted、max_turns、prompt_too_long 等)。

3.2 循环状态

每次循环迭代之间传递的可变状态被封装在一个 State 对象中:

// src/query.ts
type State = {
  messages: Message[]
  toolUseContext: ToolUseContext
  autoCompactTracking: AutoCompactTrackingState | undefined
  maxOutputTokensRecoveryCount: number
  hasAttemptedReactiveCompact: boolean
  maxOutputTokensOverride: number | undefined
  pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
  stopHookActive: boolean | undefined
  turnCount: number
  transition: Continue | undefined  // 上一次迭代为什么继续了
}

我在读这段代码时注意到一个设计细节:transition 字段记录了上一次循环"为什么继续"。注释说这是为了让测试能断言恢复路径(recovery path)是否触发了,而不需要检查消息内容。这是一种很实用的可观测性设计。

3.3 单次迭代的完整流程

每一次 while(true) 迭代就是一个完整的 think-act 周期。我把读源码时整理出来的流程画成了这张图:

┌────────────────────────────────────────────────────┐
│                单次迭代开始                           │
│                                                    │
│  1. 上下文预处理管道                                  │
│     applyToolResultBudget (大结果持久化到磁盘)        │
│     ↓                                              │
│     snipCompact (裁剪历史消息)                       │
│     ↓                                              │
│     microcompact (消息级微优化)                      │
│     ↓                                              │
│     contextCollapse (上下文折叠投影)                  │
│     ↓                                              │
│     autoCompact (全对话摘要)                         │
│                                                    │
│  2. 调用模型 API(流式)                              │
│     deps.callModel() → streaming                   │
│     ↓                                              │
│     检测 tool_use blocks → StreamingToolExecutor    │
│     (模型还在生成时就开始执行工具!)                     │
│                                                    │
│  3. 错误恢复                                         │
│     prompt-too-long → 上下文折叠 / 响应式压缩         │
│     max-output-tokens → 递增限制重试(最多3次)        │
│     模型故障 → fallback 到备用模型                    │
│                                                    │
│  4. 工具执行                                         │
│     partitionToolCalls → 并发安全 / 串行批次          │
│     runToolsConcurrently / runToolsSerially         │
│                                                    │
│  5. 续行判断                                         │
│     有 tool_use → needsFollowUp=truecontinue    │
│     stopHook 注入 → retry                           │
│     token budget 未耗尽 → continue                  │
│     以上都不是 → return Terminal                     │
│                                                    │
└────────────────────────────────────────────────────┘

3.4 为什么用异步生成器?

这个选择非常值得说道。用 async function* 而不是回调或 Promise 链有几个关键好处:

  1. 背压控制(backpressure):调用方可以按自己的节奏消费事件,REPL 可以渲染 UI,SDK 可以序列化传输
  2. 生命周期语义清晰yield 是中间事件,return 是终态,throw 是异常——三种语义天然分离
  3. 可组合yield* 可以把子生成器的所有事件透传给父生成器
  4. 取消传播:通过 .return() 可以级联关闭所有嵌套生成器

对比之下,很多 Agent 框架用的是 callback 或 EventEmitter,在错误恢复和取消传播上会复杂得多。

3.5 流式工具执行——Claude Code 快的秘密

我在读源码时发现了一个"偷跑"机制:StreamingToolExecutor

// src/services/tools/StreamingToolExecutor.ts
export class StreamingToolExecutor {
  private tools: TrackedTool[] = []
  private hasErrored = false
  private siblingAbortController: AbortController

  addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
    // 工具从流中解析出来时就立刻加入执行队列
    // 不需要等待完整的模型响应
  }
}

当模型的流式响应中出现 tool_use block 时,StreamingToolExecutor 立刻开始执行这个工具,不等模型响应完成。这意味着当模型还在生成第 2、3 个工具调用时,第 1 个工具可能已经执行完了。

更精妙的是它的并发控制:

  • 并发安全的工具(如 Read、Grep、Glob)可以并行执行
  • 非并发安全的工具(如 Bash、Edit)独占执行
  • 当某个 Bash 工具报错时,siblingAbortController 立即杀掉兄弟进程,但不影响父级——查询循环继续运行

这个设计让 Claude Code 在多文件读取等场景下有明显的速度优势。

四、工具系统:47 把瑞士军刀

4.1 Tool 抽象层

所有工具统一由 buildTool() 工厂函数构建,核心类型定义在 src/Tool.ts

// src/Tool.ts (简化)
export type Tool<Input, Output, Progress> = {
  name: string
  inputSchema: ZodSchema                          // Zod schema 验证输入
  call(args, context, canUseTool): Promise<ToolResult<Output>>
  prompt(options): Promise<string>                // 贡献到系统 prompt
  checkPermissions(input, context): Promise<PermissionResult>
  isConcurrencySafe(input): boolean              // 能否并行执行?
  isReadOnly(input): boolean                     // 是否写文件系统?
  isDestructive?(input): boolean                 // 是否不可逆?
  maxResultSizeChars: number                     // 超过多少持久化到磁盘
  // ... 还有渲染、进度、UI 等 ~40 个方法
}

关键设计:默认值是 fail-closed 的

const TOOL_DEFAULTS = {
  isConcurrencySafe: (_input?: unknown) => false,  // 默认不安全
  isReadOnly: (_input?: unknown) => false,          // 默认假设会写
  isDestructive: (_input?: unknown) => false,
}

也就是说,如果一个新工具忘了声明自己是并发安全的,它会自动被串行执行。这种"安全默认值"的设计在大型系统中非常重要。

4.2 工具注册:条件编译与死代码消除

src/tools.ts 中的 getAllBaseTools() 是所有内置工具的注册中心。我在读这段代码时被它的条件编译机制震撼到了:

// src/tools.ts
import { feature } from 'bun:bundle'

const SleepTool = feature('PROACTIVE') || feature('KAIROS')
  ? require('./tools/SleepTool/SleepTool.js').SleepTool
  : null

const CtxInspectTool = feature('CONTEXT_COLLAPSE')
  ? require('./tools/CtxInspectTool/CtxInspectTool.js').CtxInspectTool
  : null

// Ant 内部专用工具
const REPLTool = process.env.USER_TYPE === 'ant'
  ? require('./tools/REPLTool/REPLTool.js').REPLTool
  : null

feature() 来自 bun:bundle,是一个构建时常量。Bun 打包器会在编译期求值这些表达式,对外部构建,feature('KAIROS') 永远是 false,整个 require() 分支会被彻底消除——连字符串字面量都不会留在产物中。

这意味着 Claude Code 的外部发布版和内部版本共享同一套源码,但通过构建时 feature flag 产出完全不同的制品。这比运行时 feature flag 干净得多。

4.3 工具调度:智能分批

src/services/tools/toolOrchestration.ts 实现了工具调度的核心逻辑:

// src/services/tools/toolOrchestration.ts
function partitionToolCalls(
  toolUseMessages: ToolUseBlock[],
  toolUseContext: ToolUseContext,
): Batch[] {
  return toolUseMessages.reduce((acc: Batch[], toolUse) => {
    const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
    const isConcurrencySafe = tool?.isConcurrencySafe(parsedInput) ?? false

    // 连续的并发安全工具合并为一个批次(并行)
    // 非安全工具各自一个批次(串行)
    if (isConcurrencySafe && lastBatch?.isConcurrencySafe) {
      lastBatch.blocks.push(toolUse)
    } else {
      acc.push({ isConcurrencySafe, blocks: [toolUse] })
    }
    return acc
  }, [])
}

这里的关键洞察:并发安全性是 per-invocation 的,不是 per-tool-type 的。同一个 BashTool,ls 是并发安全的,rm -rf 不是。这种粒度的控制是通过 isConcurrencySafe(input) 实现的——传入的是具体的调用参数。

4.4 几个值得深究的工具实现

FileEditTool:搜索替换而不是行号编辑

// src/tools/FileEditTool/FileEditTool.ts (简化)
// 输入: file_path, old_string, new_string, replace_all

Claude Code 选择了搜索替换而不是行号编辑来修改文件。为什么?

因为行号编辑有一个致命缺陷:当模型产生的上下文与实际文件有偏差时(比如文件被外部修改了),行号会对不上。搜索替换则更鲁棒——只要目标文本存在,不管它在哪一行。

这个工具内置了多重安全机制:

  1. Must-Read-First Guard:如果模型没有读过某个文件,就不允许编辑它,防止盲改
  2. Stale Write Detection:通过 readFileState 追踪文件的修改时间戳。如果文件在上次读取后被外部修改了,编辑被拒绝
  3. Duplicate Match Safety:如果 old_string 在文件中匹配多处但 replace_all 为 false,操作被拒绝并返回匹配数量
  4. Quote NormalizationfindActualString() 处理中英文引号不一致的情况
  5. File History:每次编辑前创建备份用于 undo

BashTool:最复杂的工具

BashTool 是整个系统中最复杂的工具实现,约 1800 行代码。它的权限系统 (bashPermissions.ts) 特别值得关注:

  • 使用 tree-sitter 对 Bash 命令进行 AST 级解析
  • 将管道命令拆分为子命令(硬限 50 个,防止 DoS)
  • 每个子命令独立匹配权限规则(精确匹配、前缀匹配、通配符匹配)
  • auto 权限模式下,还有一个 LLM 分类器对命令进行安全评估

还有一个有趣的细节:_simulatedSedEdit 内部字段。当 Bash 命令包含 sed 编辑时,权限对话框会预先计算 sed 的执行结果,让用户在审批时就能看到"这条命令会把文件改成什么样"。

AgentTool:子 Agent 编排

AgentTool 是多 Agent 架构的核心,支持多种执行模式:

// src/tools/AgentTool/AgentTool.tsx (输入 schema 简化)
z.object({
  description: z.string(),      // 3-5 词任务描述
  prompt: z.string(),            // 完整任务提示
  subagent_type: z.string(),     // 专用 Agent 类型
  model: z.enum(['sonnet', 'opus', 'haiku']),
  run_in_background: z.boolean(),
  isolation: z.enum(['worktree']),  // git worktree 隔离
})

支持的模式包括:

  • 同步子 Agent:行内执行,返回结果
  • 异步后台 Agent:立即返回 agentId,后台运行
  • Worktree 隔离:创建临时 git worktree,子 Agent 在隔离的仓库副本上工作
  • Coordinator 模式:主 Agent 只有 AgentTool / SendMessageTool / TaskStopTool,worker Agent 拥有实际工具——同一个循环代码,通过工具集配置变成完全不同的角色

五、上下文管理:5 层压缩管道

这可能是 Claude Code 源码中最精妙的部分。上下文窗口管理不是简单的"截断旧消息",而是一个 5 层逐级压缩的管道,每一层有不同的触发条件、代价和粒度。

5.1 管道全景

Layer 1: Tool Result Budget (每条消息限额)
    ↓ 大的工具结果持久化到磁盘,替换为预览
Layer 2: Snip Compact (历史裁剪)
    ↓ 移除对话中间的旧消息
Layer 3: Microcompact (消息级微优化)
    ↓ 编辑单条消息内容,不破坏 prompt cache
Layer 4: Context Collapse (上下文折叠)
    ↓ 读时投影,摘要存在独立的 collapse store
Layer 5: Auto Compact (全对话摘要)
    ↓ Fork 一个独立 Agent 生成对话摘要
        ↓ (应急) Reactive Compact
            当 API 返回 prompt-too-long 时紧急触发

5.2 每层的实现细节

我在源码中找到了 Auto Compact 的触发阈值计算:

// src/services/compact/autoCompact.ts
export function getAutoCompactThreshold(model: string): number {
  const effectiveContextWindow = getEffectiveContextWindowSize(model)
  return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS  // 缓冲 13,000 tokens
}

当估计 token 数超过 contextWindow - 13000 - maxOutputTokens 时触发。Auto Compact 使用一个独立的 API 调用(fork agent)来生成摘要,避免阻塞主循环。

更巧妙的是 Reactive Compact——当 API 返回 prompt-too-long 错误时,这个错误会被**扣留(withhold)**不传给调用方,然后紧急触发压缩。如果压缩成功,循环无感知地继续;如果失败,错误才会浮出。

// src/query.ts 中的错误恢复逻辑(简化)
// prompt-too-long 恢复路径:
// 1. 先尝试 context collapse drain
// 2. 再尝试 reactive compact
// 3. 都失败了才让错误浮出

Auto Compact 还有一个熔断器:连续失败 3 次后,停止重试。这防止了在上下文真的无法压缩时(比如系统 prompt 本身就很大)陷入无限重试。

5.3 为什么需要 5 层?

每一层解决不同的问题:

解决的问题代价
Tool Result Budget单条消息太大磁盘 I/O
Snip Compact历史太长丢失旧上下文
Microcompact缓存命中率优化微小的信息损失
Context Collapse渐进式上下文缩减摘要质量
Auto Compact整体超限一次 API 调用 + 信息压缩

层次化设计意味着轻量操作先执行。如果 Snip 就够了,就不会触发昂贵的 Auto Compact。如果 Context Collapse 把 token 数压到了阈值以下,Auto Compact 直接跳过。

六、权限系统:三层纵深防御

Claude Code 能直接修改文件和执行命令,权限系统就是安全命脉。源码中实现了三层纵深防御。

6.1 Layer 1: 规则匹配

// src/utils/permissions/permissions.ts (简化)
async function hasPermissionsToUseToolInner(tool, input, context) {
  // 1a. 整个工具被 deny?
  const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
  if (denyRule) return { behavior: 'deny' }

  // 1b. 整个工具需要 ask?
  const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
  if (askRule) return { behavior: 'ask' }

  // 1c. 工具自身的权限检查
  return await tool.checkPermissions(parsedInput, context)
}

规则来自多个源,按优先级排序:policySettingsuserSettingsprojectSettingscliArgcommandsession

规则语法支持模式匹配:

  • Bash(git *) — 允许所有 git 开头的命令
  • Edit(/src/**) — 允许编辑 /src/ 下的文件
  • mcp__server1 — 禁用某个 MCP 服务器的所有工具

6.2 Layer 2: 工具专属检查

每个工具实现自己的 checkPermissions()。比如文件工具会检查:

  • 文件路径是否在允许的工作目录内
  • 是否是 UNC 路径(防止 Windows NTLM 凭据泄露)
  • 是否包含敏感文件(.env、凭据文件等)

BashTool 的检查更复杂:

  • tree-sitter AST 解析命令
  • 每个子命令独立匹配规则
  • 路径约束检查
  • Sed 编辑检测与验证

6.3 Layer 3: 交互式审批

当前两层返回 behavior: 'ask' 时,进入交互式流程:

hasPermissionsToUseToolInner() → 'ask'
    ↓
useCanUseTool hook
    ↓
handleCoordinatorPermission() (协调者 Agent)
    ↓
Bash Classifier (LLM 分类器预判)
    ↓
handleInteractivePermission() (用户审批 UI)

Bash Classifier 是一个值得注意的设计:在 auto 模式下,一个轻量 LLM 会对命令进行安全评估。如果分类器认为安全,可以跳过用户确认。这是对"安全性"和"流畅性"的折中——既不像 bypassPermissions 那样完全放弃安全检查,也不像 default 那样每次都弹窗。

七、状态管理:34 行代码的极简 Store

Claude Code 没有用 Redux、Zustand 或任何状态管理库,而是用 34 行代码实现了自己的 store:

// src/state/store.ts — 完整源码
type Listener = () => void
type OnChange<T> = (args: { newState: T; oldState: T }) => void

export type Store<T> = {
  getState: () => T
  setState: (updater: (prev: T) => T) => void
  subscribe: (listener: Listener) => () => void
}

export function createStore<T>(
  initialState: T,
  onChange?: OnChange<T>,
): Store<T> {
  let state = initialState
  const listeners = new Set<Listener>()

  return {
    getState: () => state,
    setState: (updater: (prev: T) => T) => {
      const prev = state
      const next = updater(prev)
      if (Object.is(next, prev)) return  // 引用相等跳过
      state = next
      onChange?.({ newState: next, oldState: prev })
      for (const listener of listeners) listener()
    },
    subscribe: (listener: Listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    },
  }
}

用 React 的 useSyncExternalStore 搭配选择器订阅,只有被选中的 state 切片变化时才触发 re-render。这套方案在 CLI 场景下足够了——不需要中间件、devtools、时间旅行调试等 Web 应用常用的功能。

AppState 本身使用了 DeepImmutable<T> 类型约束,编译期强制不可变。

八、提示词工程:缓存友好的双层结构

8.1 System Prompt 的组装

src/constants/prompts.ts 中的 getSystemPrompt() 返回一个 string[]——字符串数组而非单个字符串。这是为了支持两层缓存

// src/constants/prompts.ts (简化)
export async function getSystemPrompt(tools, model): Promise<string[]> {
  return [
    // --- 静态内容(全局可缓存)---
    getSimpleIntroSection(),          // 身份介绍
    getSimpleSystemSection(),          // 核心系统规则
    getSimpleDoingTasksSection(),      // 编码行为指导
    getActionsSection(),               // 可逆性/影响范围指导
    getUsingYourToolsSection(tools),   // 工具使用偏好
    getSimpleToneAndStyleSection(),    // 沟通风格

    // === 动态内容边界 ===
    ...(shouldUseGlobalCacheScope()
      ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY]
      : []),

    // --- 动态内容(每会话)---
    ...resolvedDynamicSections,       // Memory、环境信息、MCP 指令等
  ].filter(s => s !== null)
}

SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之前的内容可以跨用户/跨会话复用缓存(Anthropic API 的 prompt caching 功能),之后的内容每个会话独立。这直接影响 API 成本——缓存命中的 token 价格远低于全量输入。

8.2 动态 Prompt Section 的记忆化

每个 prompt section 都通过 systemPromptSection() 包装,实现会话级记忆化——相同参数只计算一次:

// src/constants/systemPromptSections.ts (概念)
export function systemPromptSection(computeFn) {
  // 一个 session 内只计算一次
  // 后续调用直接返回缓存结果
}

还有 DANGEROUS_uncachedSystemPromptSection()——每个 turn 重新计算。用于 MCP 服务器指令这种可能在会话中途变化的内容(MCP 服务器可以动态连接/断开)。

8.3 Latch 机制

Bootstrap state 中有多个"锁存器"(latch)变量:

afkModeHeaderLatched
fastModeHeaderLatched
cacheEditingHeaderLatched
thinkingClearLatched

这些变量一旦设为 true 就不会回退为 false。目的:如果 fast mode 在会话中途开启然后关闭,发送给 API 的 header 保持"开启过"状态,避免因为 header 变化导致 prompt cache 失效。

这种对缓存命中率的极致优化在整个代码库中随处可见。

九、技能与插件系统

9.1 Skill 系统

Skills 是 Markdown 文件驱动的能力扩展:

// src/skills/bundledSkills.ts (简化)
export type BundledSkillDefinition = {
  name: string
  description: string
  whenToUse?: string              // 模型何时应该调用这个 Skill
  allowedTools?: string[]          // 限制可用工具
  model?: string                   // 覆盖模型
  context?: 'inline' | 'fork'     // 内联执行 or fork 子 Agent
  hooks?: HooksSettings            // 每 Skill 的 hook 配置
  getPromptForCommand: (args, context) => Promise<ContentBlockParam[]>
}

Skill 的加载源有明确优先级:

  1. bundled — 编译时内置
  2. plugin — 插件提供
  3. skills.claude/skills/ 目录的 Markdown 文件
  4. managed — 组织策略管理

一个特别有趣的特性是条件激活activateConditionalSkillsForPaths() 会在 FileEditTool、FileReadTool、FileWriteTool 每次操作文件时被调用。当用户操作某些目录或文件模式时,对应的 Skill 会自动激活。

9.2 插件系统

插件分三个层级:

  • Built-in plugins:随 CLI 发布,用户可启用/禁用
  • Bundled skills:硬编码的复杂功能
  • Marketplace plugins:从 Git 仓库加载,支持 SHA 版本锁定

一个插件可以提供:Commands、Agents、Skills、Hooks、MCP Servers、LSP Servers、Output Styles、Settings——几乎可以扩展系统的所有方面。

错误处理方面,源码定义了 25+ 种 PluginError 类型的联合类型,每种都携带特定的上下文信息。

9.3 Memory 系统

src/memdir/ 实现了文件级的持久化记忆:

  • 存储路径:~/.claude/projects/<项目哈希>/memory/
  • 入口文件:MEMORY.md(始终加载到上下文,限 200 行 25KB)
  • 独立话题文件:按需通过 AI 侧查询召回(用 Sonnet 模型选择最多 5 个相关文件)
  • 四种记忆类型:user(用户信息)、feedback(纠正/确认)、project(项目上下文)、reference(外部系统指针)

安全方面,validateMemoryPath() 拒绝相对路径、根路径、UNC 路径、空字节、长度不足 3 的路径;并且明确排除 projectSettings 作为 autoMemoryDirectory 的来源——防止恶意仓库将内存写入重定向到 ~/.ssh

十、任务系统与多 Agent 协作

10.1 Task 系统

src/tasks/ 不是简单的 Todo List,而是一个并发任务执行管理器,支持 7 种任务类型:

type TaskType =
  | 'local_bash'           // 后台 shell
  | 'local_agent'          // 本地子 Agent
  | 'remote_agent'         // 远程 Agent
  | 'in_process_teammate'  // 进程内协作者
  | 'local_workflow'       // 工作流
  | 'monitor_mcp'          // MCP 监控
  | 'dream'                // 自动记忆整理

Task ID 使用 randomBytes(8) 生成(36^8 ≈ 2.8 万亿种组合),注释中明确提到这是为了防止符号链接攻击

10.2 DreamTask:AI 的"睡眠整理"

DreamTask 是一个自动记忆整理子 Agent——当触发条件满足时,它会回顾最近的对话,将有价值的信息提炼到持久化记忆文件中。如果被中途 kill,它会回滚整理锁的时间戳,让下次会话可以重试。这个设计灵感明显来自人类的睡眠记忆整理机制。

10.3 后台主会话

当用户按两次 Ctrl+B 时,LocalMainSessionTask 把当前对话推到后台。后台运行的查询有独立的 transcript 文件,发送进度更新(工具数、token 数),完成时发出 XML 通知。这让用户可以在等待长任务时继续交互。

10.4 Stall Watchdog

LocalShellTask 内置了一个卡死检测器:

  • 每 5 秒检查后台命令是否有新输出
  • 45 秒无输出后,检查最后 1024 字节是否包含交互提示((y/n)Press any keyContinue?
  • 如果检测到,通知用户建议使用非交互标志

这种主动监控机制在其他 Agent 工具中很少见到。

十一、Claude Code 强大的关键原因

从源码分析角度,我总结 Claude Code 强于其他 AI Coding Agent 的根本原因:

11.1 端到端的工程化闭环

不是简单地把 LLM 包一层 API——从输入解析、提示词组装、上下文管理、工具调度、权限控制、错误恢复到输出渲染,每个环节都经过精心设计。

11.2 上下文管理的工程深度

5 层压缩管道 + 缓存优化 + reactive 恢复 + 熔断器。这套系统解决的核心问题是:让模型在有限的上下文窗口中始终看到最相关的信息

大多数 Agent 框架的上下文管理是"满了就截断",Claude Code 的策略是"分层递进压缩,轻量操作优先,重量操作兜底"。

11.3 安全模型的深度

三层权限系统 + AST 级命令分析 + LLM 分类器 + UNC 路径防护 + stale write 检测 + must-read-first guard。这些不是"加个确认弹窗"能比的——每一层都在解决不同的攻击面。

11.4 Streaming 执行的性能优势

模型生成和工具执行重叠进行,加上智能并发分批,让 Claude Code 在多文件操作场景下的延迟远低于"等生成完再执行"的串行方案。

11.5 设计上的 trade-off 意识

  • 用异步生成器而非 callback——牺牲一点学习成本,换来背压控制和生命周期清晰
  • 用 fail-closed 默认值——牺牲一点灵活性,换来安全底线
  • 用 5 层上下文管道而非简单截断——增加了复杂度,但换来了上下文利用率的质的飞跃
  • 用构建时 feature flag——维护一套源码而非两个仓库,但需要 Bun 打包器支持

十二、可复用的设计模式

从 Claude Code 源码中提炼出的可迁移经验:

12.1 Agent Loop 设计模式

async function* agentLoop(params):
  while (true):
    preprocess context         // 上下文预处理管道
    response = callModel()     // 调用模型
    if error: recover or break // 错误恢复
    if tool_use:
      results = executeTool()  // 工具执行
      yield results            // 向外暴露事件
      continue                 // 继续循环
    else:
      return terminal          // 循环结束

关键点:

  • 用生成器做主循环,yield 暴露中间事件
  • 状态封装在 State 对象中,continue 站点统一赋值
  • 错误恢复在循环内部处理,不向外抛出

12.2 Tool 抽象模式

Tool = {
  inputSchema,          // 输入验证(Zod)
  call(),               // 执行逻辑
  checkPermissions(),   // 权限检查
  isConcurrencySafe(),  // 并发安全性声明
  isReadOnly(),         // 只读声明
  prompt(),             // 对系统 prompt 的贡献
}

关键点:

  • 每个工具自描述(包括 prompt 贡献和安全属性)
  • 默认值 fail-closed
  • 并发安全性是 per-invocation 的

12.3 分层上下文管理模式

轻量级(零/低 API 成本)→ 中量级(局部优化)→ 重量级(全量压缩)→ 应急(reactive)

关键点:

  • 每层独立判断是否需要介入
  • 前面的层如果解决了问题,后面的层直接跳过
  • 最后一层是应急手段,有熔断器

12.4 权限 Layered Defense

Layer 1: 静态规则匹配(快,零成本)
Layer 2: 工具专属检查(中等,可能有 I/O)
Layer 3: LLM 分类器 + 用户交互(慢,但最灵活)

关键点:

  • 大部分请求在 Layer 1 就已决定(命中 allow/deny 规则)
  • 只有不确定的情况才向上层升级
  • 每一层都可以独立运作

12.5 State Store 模式

34 行的极简 store + DeepImmutable 类型约束 + useSyncExternalStore 选择器。证明了在很多场景下,你不需要 Redux。

12.6 Skill 扩展模式

用 Markdown + YAML frontmatter 定义能力扩展。降低扩展门槛——写一个 .md 文件就能给 Agent 添加新能力,不需要写代码。条件激活机制让 Skill 能根据当前操作的文件类型自动加载。

十三、如果我要自己实现一个类似系统

技术选型建议

组件推荐原因
运行时Bun启动快,原生 TS,feature() 构建时消除
Agent Loopasync generator背压控制,生命周期清晰
输入验证Zod运行时验证 + 类型推导
CLI UIInk (React for terminal)组件化,React 生态复用
命令解析Commander.js成熟稳定
状态管理自建微型 Store30 行就够,别引入 Redux
文件搜索ripgrep (rg) 子进程速度碾压原生 Node glob
命令分析tree-sitterAST 级精度
LLM APIAnthropic SDK + streaming原生流式支持

实现路径建议

  1. 先实现最小 Agent Loopwhile(true) → callModel → executeTools → yield,不需要一开始就搞 5 层上下文管理
  2. Tool 系统用 interface + factory:一开始 3-5 个核心工具(Read、Edit、Bash、Glob、Grep)就够了
  3. 权限系统从 Layer 1 开始:静态规则匹配覆盖 80% 场景,分类器和交互式审批后加
  4. 上下文管理逐层加:先做简单截断,然后加 Auto Compact,再加微优化层
  5. 子 Agent 能力放最后:单 Agent 先跑通,多 Agent 编排是进阶能力

十四、结语

读完 Claude Code 的源码,我最大的感受不是"它有多少 fancy 的技术",而是每一个设计决策背后都有明确的工程权衡

异步生成器不是为了炫技,是因为它真的解决了 Agent 循环中事件流控制的核心问题。5 层上下文管道不是过度设计,是因为 LLM 的上下文窗口就是 Agent 系统最关键的资源瓶颈。34 行的 Store 不是偷懒,是因为 CLI 工具确实不需要 Redux 那套东西。

Claude Code 证明了一件事:AI Agent 的竞争力不仅在模型能力上,更在工程化的深度上。同样的基座模型,配上精心设计的工具系统、上下文管理、权限控制和错误恢复,体验可以天差地别。

这也意味着,AI Agent 领域的竞争正在从"谁的模型更强"转向"谁的 Agent 工程做得更好"。Claude Code 的源码为这个方向提供了一个高水平的参考实现——无论你是在构建自己的 Coding Agent,还是在设计其他领域的 Agent 系统,都能从中找到可借鉴的工程模式。

源码面前,了无秘密。但理解设计意图,需要一点工程直觉。希望这篇文章能帮你建立这种直觉。