Claude Code 源码泄漏,我从 4756 个文件里扒出了这些秘密
一只 ASCII 鸭子、20 道安全防线、和一个名叫 KAIROS 的幽灵——这是 Anthropic 不小心公开的工程哲学。
今天,技术社区炸了锅。
有人发现 Anthropic 发布在 npm 上的 @anthropic-ai/claude-code 包里,附带了一份完整的 source map 文件——cli.js.map,体积接近 60MB。通过提取其中的 sourcesContent 字段,社区成员还原出了 1884 个 TypeScript 源文件,Claude Code v2.1.88 的几乎完整实现就这样摊在了所有人面前。
这不是什么”越狱”或”攻击”。Source map 是开发者自己打包进去的,就像你把日记塞进了快递箱子,收件人拆开后顺手就看到了。
严格来说,这份还原并不完整——还有几百个文件没能从 source map 中提取出来,真正的内部仓库结构也无从得知。但这不重要。已经还原出的 1884 个源文件,足以让我们窥见 Anthropic 在工程实践上的真实水准。
一个用 React 写的”命令行工具”
打开源码目录的第一眼,我就觉得不对劲——这东西也太大了。
restored-src/src/├── main.tsx # 入口:是的,一个 CLI 工具用 TSX├── tools/ # 30+ 个工具实现├── commands/ # 40+ 个斜杠命令├── services/ # API、MCP、分析服务├── coordinator/ # 多 Agent 协调引擎├── assistant/ # KAIROS 自主代理模式├── buddy/ # ASCII 宠物伙伴系统├── voice/ # 语音交互├── vim/ # Vim 模式├── plugins/ # 插件系统├── skills/ # 技能系统├── remote/ # 远程会话└── context/ # React Context
等等,React Context?一个命令行工具要 React Context 干什么?
答案是 Claude Code 的 UI 层用了 Ink——React 的终端渲染器。你在终端里看到的权限弹窗、进度条、Markdown 渲染,甚至那个你以为是纯文本的工具调用展示,全都是 React 组件。入口文件叫 main.tsx 不是写错了,在 Anthropic 的工程师眼里,终端就是另一个前端。
后面看到 bootstrap/state.ts 里还有一段滚动性能优化,就更印证了这个判断——用户滚动时所有后台工作暂停 150ms,Timer 还得 .unref() 避免阻止进程退出。这分明就是在做一个 GUI app。
知道了这个前提,再看后面的东西就好理解了。
47 个方法的”工具”到底是什么
Claude Code 的核心抽象是 Tool 接口,定义在 Tool.ts 里。我原以为一个”工具”就是个函数——接收输入,返回结果。但实际上它是一个拥有 47 个属性和方法的庞然大物:
type Tool<Input, Output, P> = { name: string inputSchema: ZodSchema prompt(): string// 告诉 AI 这个工具是什么 call(): Promise<Output> // 实际执行 checkPermissions(): Promise<PermissionResult> // 权限检查 isReadOnly(): boolean// 是否只读 isConcurrencySafe(): boolean// 是否并发安全 isDestructive(): boolean// 是否有破坏性 interruptBehavior: 'cancel' | 'block'// 中断策略 renderToolUseMessage() // UI 渲染(调用时) renderToolResultMessage() // UI 渲染(结果时) toAutoClassifierInput() // 自动分类器输入 searchHint: string// 工具搜索关键词// ... 还有 30 多个}
每个工具不只负责”干活”,它还要知道如何展示自己、如何被搜索到、如何处理中断、如何向 AI 描述自己。一个工具就是一个完整的生命周期。
所有工具通过 buildTool 工厂函数创建。这个函数有一组默认值,值得细看:
const TOOL_DEFAULTS = { isEnabled: () =>true, isConcurrencySafe: (_input?) =>false, // 默认不安全 isReadOnly: (_input?) =>false, // 默认有写操作 isDestructive: (_input?) =>false, checkPermissions: (input, _ctx?) =>Promise.resolve({ behavior: 'allow', updatedInput: input }), // 交给通用权限系统 toAutoClassifierInput: (_input?) =>'', userFacingName: (_input?) =>'',}
isConcurrencySafe 和 isReadOnly 默认全是 false——你写了一个新工具但忘声明安全性?没关系,系统会把你当作最危险的那种来对待:不可并发、有写操作。checkPermissions 默认 allow 看着像是放行,其实是把皮球踢给了更外层的通用权限系统。工具层默认假设最坏情况,权限层做最终裁决。
这种思路在后面的 Bash 安全模块里被推到了极致。
Bash 安全:一封写给攻击者的情书
如果只看一个模块就想理解 Anthropic 的工程文化,那一定是 BashTool。
让 AI 执行终端命令大概是整个产品里最吓人的功能。他们的应对方式不是加一道防线,而是堆了一座纵深工事。光 bashSecurity.ts 一个文件就塞了 23 个安全校验器,每条命令进来要过一遍这个流水线:
validateEmpty() // 空命令validateSafeCommandSubstitution() // 安全的 heredoc 模式validateGitCommit() // git commit 特殊处理validateIncompleteCommands() // 不完整命令碎片validateJqCommand() // jq 的 system() 函数validateShellMetacharacters() // ;|& 等元字符validateDangerousVariables() // 变量靠近重定向validateDangerousPatterns() // 反引号、$()、${}validateRedirections() // < 和 > 操作符validateNewlines() // 换行分割命令validateCarriageReturn() // CR 回车注入攻击validateIFSInjection() // $IFS 操纵validateProcEnvironAccess() // /proc/*/environ 访问validateMalformedTokenInjection() // 不平衡的分隔符validateObfuscatedFlags() // ANSI-C 引号混淆validateBackslashEscapedWhitespace() // 反斜杠转义空格validateBraceExpansion() // {a,b} 大括号展开validateUnicodeWhitespace() // Unicode 空白字符validateMidWordHash() // 单词中间的 # 号validateCommentQuoteDesync() // 注释引号不同步validateQuotedNewline() // 双引号内换行
每个校验器的注释都解释了自己在防御什么攻击,很多引用了 HackerOne 报告编号。随便挑两个说说:
validateUnicodeWhitespace() 防的是 Unicode 中”看起来像空格但不是空格”的字符,比如 U+00A0(不间断空格)。Bash 不会把它当分隔符,人眼却分不出来。攻击者可以构造一条看起来无害的命令,实际上因为空格不是空格而完全变了意思。
validateCarriageReturn() 防的是回车注入——\r 在某些终端里可以让后面的文本覆盖前面的显示。命令在屏幕上”看起来”是 ls,实际执行的是 rm -rf /。
然后是 Zsh 的专项治理。因为 macOS 默认 shell 是 Zsh,而 Zsh 有大量 Bash 里不存在的内建命令,Claude Code 维护了一个专门的黑名单:
const ZSH_DANGEROUS_COMMANDS = new Set(['zmodload', // 模块加载——万恶之源'emulate', // -c 标志等价于 eval'sysopen', 'sysread', 'syswrite', 'sysseek', // 文件描述符操作'zpty', // 伪终端命令执行'ztcp', // TCP 连接(数据泄露)'zsocket', // Unix/TCP socket'zf_rm', 'zf_mv', 'zf_ln', 'zf_chmod', // 绕过二进制检查的内建命令'zf_chown', 'zf_mkdir', 'zf_rmdir', 'zf_chgrp',])
zmodload 能加载 zsh/files 模块,让 zf_rm 这种内建命令绕过 rm 二进制的检查。ztcp 能直接建 TCP 连接,不需要 curl 或 wget。就算你封禁了所有常见危险程序,Zsh 的内建照样能完成同样的操作。
但这 23 个校验器还只是外层。它们背后是一个统一的权限系统,每次工具调用都要走一遍四态判定:
type PermissionResult = | { behavior: 'allow' } // 放行 | { behavior: 'deny' } // 拒绝 | { behavior: 'ask' } // 询问用户 | { behavior: 'passthrough' } // 继续检查下一条规则
权限上下文用 DeepImmutable<> 包裹——不是靠约定说”别改”,是靠 TypeScript 类型系统从编译层面让你改不了。
具体到 Bash 工具,权限检查是一个 10 步的级联瀑布:先用 tree-sitter 做 AST 解析(没有就退到 shell-quote),然后过沙箱规则、精确匹配规则、AI 分类器判断、操作符权限、前缀匹配、sed 约束、模式权限、只读放行,最后兜底拒绝。
是的,第 4 步是用一个小模型来评估命令安全性。不是正则,是 AI 判断”这条命令在当前上下文下是否安全”。这大概是我见过的最有创意的安全设计。
还有个小细节:权限匹配前会先剥离”安全包装器”。timeout 10 git push 和 git push 在权限系统眼里是一样的——timeout 是无害前缀。但 PATH=/evil git push 不会被简化,因为 PATH 能劫持命令查找。安全的环境变量(NODE_ENV、TZ)剥离,危险的(PATH、LD_PRELOAD、PYTHONPATH)保留。
这整套东西从外到里,每一层都在说同一句话:不信任任何输入。
为了省钱,他们有多拼
看安全模块的时候我以为 Anthropic 最在意的是安全。看完 prompt cache 相关代码后我发现,他们对省钱的在意程度可能不亚于安全。
背景是这样的:大模型 API 按 token 收费,Claude Code 的系统提示有大几万 token。每次对话都重新发送的话成本惊人。Prompt cache 让服务端缓存这些内容,但要求前缀完全一致——改一个字符缓存就废了。
所以他们把系统提示劈成了两半,中间放了一道硬边界:
// 警告:不要在不更新缓存逻辑的情况下删除或重新排序此标记exportconst SYSTEM_PROMPT_DYNAMIC_BOUNDARY ='__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
边界前面的东西——身份、安全指令、代码规范——对所有用户都一样,可以全局共享缓存。后面的——工作目录、模型名、MCP 配置——是用户私有的。
更狠的是全局状态里的四个”粘性锁存器”:
afkModeHeaderLatched: booleanfastModeHeaderLatched: booleancacheEditingHeaderLatched: booleanthinkingClearLatched: boolean
这是单向开关:一旦打开,整个会话都不会关。因为 API 请求头变化也会打掉 prompt cache。用户中途开了”快速模式”?对应的 HTTP header 就永远锁定为开,绝不让它来回切换。
一次缓存失效就是 50-70K token 的费用。乘以百万级用户,这些看似过度的小设计直接影响利润。这也解释了代码里到处可见的缓存保护——在这个规模上,每一次缓存命中都是钱。
这种省钱的执念甚至渗透到了对话引擎 QueryEngine.ts 的核心设计里。比如消息在 API 调用之前就会持久化到磁盘:
// 消息在 API 调用前写入 transcript// 这样即使进程在等待 API 响应时被 kill,--resume 也能恢复会话// "bare" 模式优化:脚本调用不需要 --resume,await 约 4ms(SSD),约 30ms(磁盘争用时)
表面上这是弹性设计——电脑死机后 --resume 能恢复会话。但往深了想,恢复会话意味着不需要重新发送整段对话历史,也就是不需要重新花那笔 token 钱。
QueryEngine 里还有一个我很喜欢的小设计——错误水位线。错误日志是 100 条容量的环形缓冲区,老的从头部 shift() 掉,新的从尾部进。每轮对话开始前要记住”我从哪里开始”,结束时才能报告”新增了哪些错误”。
直觉做法是记数组下标。但环形缓冲区 shift() 一次所有下标就左移一位,你的标记会漂移。Anthropic 的做法是记引用不记下标:
const errorLogWatermark = getInMemoryErrors().at(-1)
拿最后一个错误对象的引用。后面只要找”这个引用之后的所有条目”就行。对象还是那个对象,不管它在数组第几位。一个微小的选择,但背后是一种思维习惯:不要假设数据结构是静态的,用不变的东西锚定变化的东西。
电子宠物:Anthropic 工程师的浪漫
就在我以为这份源码全是严肃的安全校验和缓存优化时,撞上了 buddy/ 目录。
Claude Code 里藏了一个完整的 ASCII 电子宠物系统。18 个物种:鸭子、鹅、水滴、猫、龙、章鱼、猫头鹰、企鹅、乌龟、蜗牛、幽灵、美西螈、水豚、仙人掌、机器人、兔子、蘑菇、还有一个叫 chonk 的胖家伙。
__ /\_/\ .----. /^\ /^\ <(· )___ ( · ·) ( · · ) < · · > ( ._> ( ω ) (______) ( ~~ ) `--´ (")_(") /\/\/\/\ `-vvvv-´ 鸭子 猫 章鱼 龙
每只有 3 帧动画。可以戴帽子——皇冠、礼帽、螺旋桨帽、光环、巫师帽、毛线帽,甚至头顶一只迷你鸭子。6 种眼睛(·✦×◉@°)。还有稀有度抽卡:
exportconst RARITY_WEIGHTS = { common: 60, // 普通 60% uncommon: 25, // 稀有 25% rare: 10, // 珍贵 10% epic: 4, // 史诗 4% legendary: 1, // 传说 1%}
最妙的是这不是真·随机,你的宠物是由你的用户 ID 决定性生成的:
const SALT = 'friend-2026-401'exportfunctionroll(userId: string): Roll{const key = userId + SALTconst value = rollFrom(mulberry32(hashString(key)))return value}
Mulberry32 是个微型种子 PRNG,你的 UUID 哈希后变成种子,然后确定性地掷骰——物种、眼睛、帽子、稀有度全都唯一确定。不是随机的,但你控制不了,也作弊不了。
每只宠物还有 RPG 属性:DEBUGGING(调试)、PATIENCE(耐心)、CHAOS(混乱)、WISDOM(智慧)、SNARK(毒舌)。一个主属性,一个废属性,稀有度越高属性下限越高。只有非普通稀有度的宠物才能戴帽子。闪光(shiny)概率 1%。
连存储都讲究:宠物的”骨骼”(物种、稀有度、属性)从不存盘,每次从 userId 重新算。只存 AI 生成的名字和性格——代码里管这个叫”灵魂”。这意味着 Anthropic 调了物种数组也不会弄坏你的宠物,而且你改配置文件也伪造不了传说级。
// bones 最后展开,旧格式配置里的过时字段会被覆盖return { ...stored, ...bones }
这个系统写死了 2026 年 4 月 1 日上线:
exportfunctionisBuddyTeaserWindow(): boolean{const d = newDate()return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7}
然后我注意到了一个让我笑出声的细节。物种名全部用十六进制编码:
exportconst capybara = c(0x63,0x61,0x70,0x79,0x62,0x61,0x72,0x61) as'capybara'
注释写着:”一个物种名与 excluded-strings.txt 中的模型代号冲突。检查工具会 grep 构建输出,用运行时拼接绕过。” “capybara”(水豚)疑似是 Anthropic 某个内部模型代号。为了不让构建系统的字符串检测误报,他们把所有物种名都编码了——连 duck 都没放过。
在一个充斥着 HackerOne 报告编号和缓存失效防护的代码库里,有人花了大量时间精心设计一个 ASCII 鸭子能不能戴巫师帽。这种反差,大概就是程序员式的浪漫。
藏在里面的另一个产品:KAIROS
如果说电子宠物是彩蛋,那 KAIROS 就是 Claude Code 里藏着的另一个产品形态。
通过 feature('KAIROS') 特性标志门控,它把 Claude Code 从”你问我答”的 CLI 变成了一个长期运行的自主代理。系统提示一句话:
You are an autonomous agent. Use the available tools to do useful work.
然后是一套跟普通模式完全不同的运行机制。Agent 通过 <tick> 消息保持心跳,不需要人类输入就能持续工作。空闲时必须调用 SleepTool——因为每次”醒来”就是一次 API 调用,prompt cache 又只活 5 分钟。更有意思的是 terminalFocus 字段:它能感知用户是否在看终端,不看就更放开手脚,在看就多沟通。
记忆系统也换了。不再是一个 MEMORY.md 文件,而是每天追加日志。然后夜间有个 /dream 技能(对,字面意思,”做梦”)把日志蒸馏成主题文件。
KAIROS 还带了个 SendUserMessage 工具。在自主模式下,Agent 的普通文本输出用户大概率不会看,只有通过这个工具发的消息才会显示在主界面:
SendUserMessage is where your replies go. Text outside it is visible if the user expands the detail view, but most won't — assume unread.
从 KAIROS 再往深一层,是多 Agent 协调系统。开启后 Claude Code 从”一个 AI 用工具”变成”一个 AI 指挥一群 AI”——能启动 Worker、给 Worker 发消息、终止 Worker。工作流分四步:Research(并行调查)→ Synthesis(协调器消化结果)→ Implementation(Worker 执行)→ Verification(Worker 验证)。
这里面有一条写在系统提示里的铁律,我觉得值得所有做 Agent 编排的人看看:
Never write “based on your findings” or “based on the research.” These phrases delegate understanding to the worker instead of doing it yourself. You never hand off understanding to another worker.
“永远不要写’基于你的发现’。这句话是把理解外包了。你不能把理解交给别人。”
协调器必须自己消化所有 Worker 的输出,形成自己的判断后再发指令。不准当传话筒。
而 Fork 模式下生成的子 Agent,第一条消息开头就是:
STOP. READ THIS FIRST.You are a forked worker process. You are NOT the main agent.
KAIROS 给 AI 自由,Fork 给 AI 铁律。同一个代码库里,两种截然相反的 prompt 策略并存。
代码注释里的战争故事
最后说说散落在代码各处的注释。一个项目的注释质量,往往比代码本身更能说明团队文化。
关于 Task ID 的字符集:
// 36^8 ≈ 2.8 万亿种组合// 足以抵抗对 tmpdir 中任务输出文件的暴力符号链接攻击
有人试过猜文件名搞 symlink 攻击,所以 ID 被设计成小写字母+数字(大小写不敏感,避免文件系统问题),8 位长度给出 2.8 万亿种可能。
关于分析数据的隐私保护:
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = { ... }
类型名翻译过来就是”我已验证这里面没有代码或文件路径”。每次有人想往埋点事件里加字段,这个类型名都在提醒你:别泄露用户代码。
关于模型发布的真实数据:
// @[MODEL LAUNCH]: False-claims mitigation for Capybara v8 (29-30% FC rate vs v4's 16.7%)
Capybara v8 的”错误声明率”是 29-30%,v4 只有 16.7%。新模型在某些方面反而退步了,Anthropic 在 prompt 层面打补丁。
还有十几个特性标志透露着未来的产品方向:
feature('KAIROS')feature('COORDINATOR_MODE')feature('BUDDY')feature('VOICE_MODE')feature('ULTRAPLAN')feature('TORCH')feature('FORK_SUBAGENT')feature('DAEMON')// ... 还有更多
每一个都是 Bun 构建时的条件编译开关。ULTRAPLAN、TORCH、DREAM——光名字就让人浮想联翩。Claude Code 远不只是现在我们看到的那个 CLI,它体内跑着大量实验中的未来。
读完这 4756 个文件,最大的感受不是”这系统好复杂”,而是”这些人好谨慎”。几乎每个架构决策都有注释解释为什么——不是解释代码做了什么,而是解释为什么选了这条路而不是另一条。注释里引用着具体的 bug 号(CC-34)、PR 号(#19134, #22546, #24302)、HackerOne 报告,像一部浓缩的工程战争史。安全设计默认假设最坏。省钱设计精确到每一个 HTTP header 的切换。而在这一切之间,有人认认真真地设计了一个 ASCII 水豚能不能戴螺旋桨帽。
大概这就是 Anthropic 的工程文化:在最严肃的事情上偏执,然后在偏执的间隙里放一只鸭子。
夜雨聆风