今天(2026.3.31)发生了一件大事:Anthropic 的 Claude Code 完整源码泄露了。
起因很离谱------有人发现 npm 上 @anthropic-ai/claude-code 的包里残留了 .map 文件,map 里有个指向 Anthropic R2 存储桶的下载链接,点进去是完整的未混淆 TypeScript 源码。zip 一解压,1903 个文件,51 万行代码,全部摊在眼前。
作为一个日常使用 Claude Code 的重度用户(也是 AI Agent 方向的开发者),我立刻 clone 下来通读了一遍。下面是我觉得最有价值的发现,按照"对实际开发有参考意义"排序,不是按目录结构排。
先说技术栈
Bun + TypeScript + React + Ink。 没错,一个终端 CLI 工具的 UI 层用的是 React。
Ink 是一个把 React 组件渲染到终端的框架,Claude Code 用它做了整个 TUI 界面------你看到的那些权限弹窗、进度条、工具调用的实时展示,底下全是 JSX。
说实话一开始挺意外的,但想想也合理:终端 UI 的状态管理确实比想象中复杂(多个 Agent 并行、流式输出、用户中断...),用 React 的状态模型来管比手搓要靠谱得多。
Agentic Loop:一个 while(true) 撑起整个 Agent
整个 Claude Code 最核心的文件是 src/query.ts,1729 行,实现了完整的 agentic 循环。注意不是 QueryEngine.ts------那个是外层的会话管理。真正的"大脑"在 query.ts 里。
简化后的核心逻辑:
typescript
async function* queryLoop(params) {
let state = { messages, toolUseContext, turnCount: 1, ... }
while (true) {
// 1) 一堆预处理:裁历史、压缩上下文、预取 memory 和 skills
// 2) 调 Claude API(流式)
// 3) 一边收流一边看有没有 tool_use block
// 4) 有的话 → 检查权限 → 执行 → 结果塞回 messages → 回到 while
// 5) 没有工具调用 → 退出
}
}
看着简单,但魔鬼在细节里。我挑几个最有意思的说。
上下文管理:不是一刀切,是四把手术刀
用过 Claude Code 的人都知道,长对话到后面它会自动"压缩"。我之前以为就是简单地把早期对话摘要一下。读了源码才知道,它其实有四种不同粒度的压缩机制在同时工作:
-
HISTORY_SNIP:最精细,直接把某些特定消息范围删掉,不做任何摘要。适合清理掉已经没用的中间工具调用结果。
-
Microcompact :利用 API 层的
cache_deleted_input_tokens能力,在缓存层面做编辑。这个比较黑科技------它不改消息内容,而是告诉 API"这些 token 你缓存里有但别用了"。 -
CONTEXT_COLLAPSE:把旧的对话轮次"归档"成摘要,维护一个类似 git 提交日志的结构。每次新查询时重放这个日志。和 autocompact 的区别是它保留了结构化的归档,不是一坨摘要。
-
Autocompact:最粗暴的一种,把整个历史压缩成一段摘要。最后的兜底手段。
这四种机制不互斥,按顺序依次执行。如果 snip 和 microcompact 已经把上下文压到阈值以下了,autocompact 就不触发。
这给我的启发挺大的------做 Agent 的上下文管理不能只有一种策略。不同场景下信息的"过期速度"不一样,需要分层处理。
流式工具并行:模型还在说话就开始干活了
这是我觉得最精巧的设计。普通的实现是:等模型说完 → 看有没有工具调用 → 有的话执行 → 结果返回 → 下一轮。Claude Code 不等。
typescript
// StreamingToolExecutor.ts
export class StreamingToolExecutor {
// 模型流式吐出一个 tool_use block,立刻开始执行
addTool(block: ToolUseBlock, message: AssistantMessage): void { ... }
// 并发安全的工具可以同时跑,写操作独占
// 结果按接收顺序排队,保证输出确定性
async *getRemainingResults(): AsyncGenerator<MessageUpdate> { ... }
}
模型还在流式输出后面的内容,前面的工具就已经在跑了。每个工具有个 isConcurrencySafe 标记------读文件、grep 这种只读操作可以并行,写文件、bash 这种需要独占。结果按接收顺序缓冲,不会乱序。
实测下来 Claude Code 的响应速度明显比 Cursor 快,有一部分原因应该就在这里。
撞到输出上限不认输
typescript
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3
模型输出撞到 max_output_tokens?循环不报错,"扣留"这个错误消息,悄悄重试,最多 3 次。对用户来说是无感的。
这段代码上面有个很有意思的注释,是一段模仿中世纪巫师口吻的话:
Heed these rules well, young wizard. For they are the rules of thinking, and the rules of thinking are the rules of the universe. If ye does not heed these rules, ye will be punished with an entire day of debugging and hair pulling.
翻译过来就是:"好好记住这些规则,年轻的巫师。如果你不听,你就等着花一整天调试和薅头发吧。"
看得出来这块代码的维护者被坑过不少次。
工具系统:不用 class 继承,全是工厂函数
我之前做 Agent 框架的工具系统,下意识会写一个 BaseTool 基类然后继承。Claude Code 完全没有继承,全是纯函数式的 buildTool():
typescript
type ToolDef<T> = {
name: string
description: string
inputSchema: ZodSchema<T> // Zod v4 做校验 + 自动生成 JSON Schema
call(input: T, ctx: ToolUseContext): AsyncGenerator<...>
isReadOnly(): boolean
getPermissions(): ToolPermission[]
renderToolUse?(input: T): ReactNode // 直接渲染到终端
getToolUseSummary?(input, result): string // 压缩上下文时的摘要
}
每个工具完全自包含:schema、权限、执行逻辑、UI 渲染、压缩摘要,全在一个文件里。没有全局注册表------每个 session 动态组装工具池,可以混合静态工具、MCP 工具、Agent 定义的工具。
40 多个工具里最复杂的是 BashTool。它不是简单 exec(command):自动分类命令类型(search/read/write),macOS 上走 sandbox-exec 沙箱,超过 15 秒的阻塞命令自动转后台,大输出存磁盘只给模型一个文件路径引用,还内置了一个 sed 命令专用解析器。
Feature Flag:我见过最优雅的功能门控
这块设计我觉得是全项目最值得偷的。Claude Code 用编译时 + 运行时两层 flag:
编译时:字符串级别的死代码消除
typescript
import { feature } from 'bun:bundle'
const voiceModule = feature('VOICE_MODE')
? require('./voice/index.js')
: null
feature() 是 Bun 的编译时宏。构建时会被替换成 true 或 false,false 的分支直接被删除------不是"不执行",是从二进制文件里物理消失,连字符串字面量都不剩。
为什么要这么做?因为安全研究员会反编译你的二进制去找隐藏功能(比如今天这次泄露...)。运行时 flag 再怎么关,字符串还在那。编译时 DCE 才是真的"不存在"。
讽刺的是,源码泄露之后这层保护就不管用了。但设计思路还是很值得学的。
我在源码里搜到十几个编译时 flag:VOICE_MODE、BRIDGE_MODE、DAEMON、KAIROS、COORDINATOR_MODE、PROACTIVE、ABLATION_BASELINE、HISTORY_SNIP、REACTIVE_COMPACT、CONTEXT_COLLAPSE、CACHED_MICROCOMPACT、CHICAGO_MCP......每一个都对应一个未发布或正在灰度的功能。
运行时:GrowthBook A/B 测试
typescript
const enabled = checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
'tengu_streaming_tool_execution2'
)
用于灰度发布和 kill switch。所有 gate 名称都是 tengu_ 前缀------tengu(天狗)大概是 Claude Code 的内部代号。从磁盘缓存读取,接受脏读,不阻塞启动。
意外发现:消融实验基础设施
有个 flag 叫 ABLATION_BASELINE,启用后会一次性关掉思考模式、上下文压缩、自动记忆、后台任务:
typescript
if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
for (const k of [
'CLAUDE_CODE_DISABLE_THINKING',
'DISABLE_COMPACT',
'DISABLE_AUTO_COMPACT',
'CLAUDE_CODE_DISABLE_AUTO_MEMORY',
'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS',
]) {
process.env[k] ??= '1';
}
}
做过 ML 的都知道消融实验是什么------逐个关掉组件看性能影响。但把这套方法论搬到产品工程上,这是我第一次在工业代码里见到。说明 Anthropic 在认真量化每个功能特性到底值不值。
隐藏功能:还没发布的东西
这部分是全文最有噱头的内容。
Voice Mode(代号 Amber Quartz)
src/voice/ 目录确认了语音模式的存在。从 voiceModeEnabled.ts 看:
- 只支持 Claude.ai OAuth 认证(API key、Bedrock、Vertex 都不行)
- 走的是一个专门的
voice_stream端点 - 有个紧急 kill switch:GrowthBook flag
tengu_amber_quartz_disabled - 从注释看已经开发到可以使用的程度了
Bridge Mode:把你的电脑变成 Claude 的远程终端
src/bridge/ 有 31 个文件,是一个完整的远程控制系统。运行 claude remote-control 之后,你的本地环境就变成一个被 claude.ai 远程操控的"桥接环境"。
最多支持 32 个并发会话,有 JWT 认证 + 可信设备机制,企业管理员可以通过策略禁用。这应该是为了让 claude.ai 网页版能直接操作用户本地的开发环境。
Buddy:终端里的电子宠物
这个是最让我没想到的。
Claude Code 内置了一个完整的虚拟宠物系统 ,而且没有用 feature flag 门控------已经在公开版本的二进制里了(只是可能还没暴露入口):
typescript
// 18 种宠物
export const SPECIES = [
duck, goose, blob, cat, dragon, octopus, owl, penguin,
turtle, snail, ghost, axolotl, capybara, cactus, robot,
rabbit, mushroom, chonk
] as const
// 5 级稀有度:普通(60%) / 罕见(25%) / 稀有(10%) / 史诗(4%) / 传说(1%)
export const RARITY_WEIGHTS = {
common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1,
}
// RPG 式属性
export const STAT_NAMES = ['DEBUGGING','PATIENCE','CHAOS','WISDOM','SNARK'] as const
还有帽子(皇冠、礼帽、螺旋桨帽、光环、巫师帽、豆豆帽、头顶小鸭子)、眼睛样式(·、✦、×、◉、@、°)、1% 概率的闪光变体。宠物属性用 Mulberry32 伪随机数生成器从你的用户 ID 确定性计算------所以每个用户的宠物是固定的,不能刷。
但最有意思的是物种名的编码方式:
typescript
// 所有物种名用 hex 编码,因为有一个名字和内部模型代号撞了
const c = String.fromCharCode
export const duck = c(0x64,0x75,0x63,0x6b) as 'duck'
export const goose = c(0x67,0x6f,0x6f,0x73,0x65) as 'goose'
export const capybara = c(0x63,0x61,0x70,0x79,0x62,0x61,0x72,0x61) as 'capybara'
// ... 全部 18 个都这样
注释原文:"One species name collides with a model-codename canary in excluded-strings.txt."
构建系统会 grep 输出文件里有没有被排除的字符串。也就是说,这 18 个物种名中有一个恰好是 Anthropic 某个未公开模型的代号。把所有名字都 hex 编码是为了防止误触发检测。
到底是哪个名字?duck、goose、blob、cat、dragon、octopus、owl、penguin、turtle、snail、ghost、axolotl、capybara、cactus、robot、rabbit、mushroom、chonk------其中一个是 Anthropic 下一个模型的代号。随便猜猜?
Skill 系统和多 Agent 协调
简单提一下这两块,因为我觉得它们的设计可以直接抄。
Skills 就是 Markdown 文件 。.claude/skills/ 目录下放 .md 文件,YAML frontmatter 里写描述、触发条件、允许的工具、用哪个模型。Claude Code 读文件时如果发现目录下有 skills,会自动加载------你甚至不用显式注册。这种"约定优于配置"的风格很 Rails。
多 Agent 协调器意外地简单。Coordinator 模式下,主 Agent 只有三个工具:生成 worker、给 worker 发消息、停止 worker。Worker 拿不到 TeamCreate 和 SendMessage------防止 worker 自己再组建团队(无限套娃)。后端支持 tmux pane、in-process、remote 三种方式。
几个容易被忽略的工程细节
隐私保护的类型安全 :埋点数据的类型名叫 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS。用类型名本身来提醒开发者"你确认过这不是代码或文件路径了吗"。简单粗暴但有效。
投机执行 :AppState 里有 speculationState,追踪每一轮的结束方式(bash 命令 / 文件编辑 / 正常结束 / 权限拒绝),用来预判下一步操作并提前执行。这解释了为什么 Claude Code 有时候"想"完就瞬间开始干活。
冷启动优化 :--version 路径做到了零 import------直接读编译时内联的版本号,一个模块都不加载就退出。其他子命令走独立的 import() 路径。只有最终进主循环才加载完整的 React 应用。
泄露原因和教训
技术上很简单:npm 发布时忘删 .map 文件,map 里引用了 R2 上的源码 zip 包,那个 URL 没有访问控制。
给所有发 npm 包的人提个醒:
package.json的files字段要白名单制,只包含你想发布的东西- CI 里加一步检查发布产物里有没有
.map文件 - 源码归档 URL 要有鉴权,别裸挂在 CDN 上
- 构建产物和源码的访问控制应该独立管理
写在最后
读完 51 万行代码,我最大的感受不是某个具体技术多牛,而是这个团队在用做研究的方法做工程。
消融实验基础设施、双层 feature flag、四种粒度的上下文管理、流式工具并行------每一个都不是拍脑袋加的,背后大概率有数据支撑。这种"每个功能都有量化验证"的工程文化,比任何单点技术都更值得学。
当然,宠物系统除外。那个纯粹是因为好玩。
作者:何宇峰 | 港大 CS 硕士 | Moonshot AI (Kimi)
本文基于公开泄露的源码进行技术分析,所有代码版权归 Anthropic 所有。