乐于分享
好东西不私藏

拆解 Claude Code 源码:51 万行 TypeScript 里藏着的"Agent 操作系统"

拆解 Claude Code 源码:51 万行 TypeScript 里藏着的"Agent 操作系统"

2026 年 3 月 31 日,Anthropic 给 npm 包 @anthropic-ai/claude-code 打包时,不小心把一个 60MB 的 cli.js.map 源映射文件塞了进去。这个文件里直接写着 Cloudflare R2 存储桶的公开地址,任何人下载 npm 包后顺着地址都能拿到 1906 个未混淆的 TypeScript 源文件,约 51.2 万行。

对你这种准备做 Agent 工程的人来说,这件事最值钱的不是八卦,而是——一份白捡的顶级工程教材。你不用再去翻官方那些只讲 API 的文档,直接看 Claude Code 的内部实现就行:核心引擎长什么样、工具怎么抽象、权限怎么分级、Hook 怎么挂、上下文怎么压。

本文讲清楚 4 件事:

  1. 骨架:四层架构 + QueryEngine 这个单文件"心脏"
  2. 一次 turn 怎么"转":代理循环 + 流式工具调用 + 权限门禁
  3. 工具与权限:Tool.ts 把"能干什么"写成了契约
  4. 扩展与安全:hooks、技能、多 Agent、上下文压缩

1,整体骨架:四层架构,引擎是"心脏",Bash 是"万能适配器"

先说结论:Claude Code 不是"LLM + Shell 聊天机器人",而是一个把权限、工具、Hook、插件、多 Agent 全部装进终端的"Agent 操作系统"。真正的循环不在入口,而在 4.6 万行的 QueryEngine.ts——这也是它和很多"100 行写个 agent"玩具最大的区别。

四层从顶到底依次是:

  • • UI 层 · React + Inkmain.tsx 入口,负责终端渲染、流式输出;启动时还会并行预取 MDM 设置、钥匙串、API 预连接。
  • • 交互层 · 命令解析commands.ts 注册 40+ 个斜杠命令(commit、review、config…),并对用户输入做预处理。
  • • 核心层 · 代理循环:本图正中那个大虚线框,里面才是 Agent 真正"转"起来的地方。
  • • 服务层 · 外部集成:Anthropic API、MCP 服务器、Git、ripgrep,全部外挂。

核心层里有 6 个关键模块,可以分成上下两排:

上排(核心三件)
下排(扩展三件)
QueryEngine.ts
(≈46K 行,代理循环)
hooks
(PreToolUse / PostToolUse / Stop)
Tool.ts
(≈29K 行,工具契约 + Zod)
skills / plugins
(可复用工作流 + 插件加载)
权限系统(default / acceptEdits / plan / bypass)
coordinator · 多 Agent
(AgentTool / TeamCreateTool)

每一层只跟相邻层交互。main.tsx 不直接调 API,它只负责把用户输入和流式结果在终端里渲染出来;commands.ts 不执行工具,它只把命令翻译成一次对话的开始;QueryEngine.ts 不直接写文件,它把"写文件"这件事委托给 Tool.ts 注册好的 Write 工具。

这里容易产生一个误解:以为主循环写在入口 main.tsx 里。不是main.tsx 在启动后只做两件事——并行预取 MDM/钥匙串/GrowthBook(顺便 lazy load OpenTelemetry 和 gRPC 两个大模块),然后初始化 React/Ink 渲染器。真正"转"起来的是 QueryEngine.ts 的 4.6 万行代码。这也解释了为什么改一个终端 UI 不会让 agent 行为变化——两层是物理隔离的。


2,一次 turn 是怎么"转"起来的:代理循环 + 流式工具调用

先说结论:Claude Code 的 agent 循环是一个"请求 → 流式响应 → 发现 tool_use → 权限检查 → 执行工具 → 结果回填 → 再请求"的闭环,直到模型不再调用工具才输出最终文本。整张循环图画成下面这样:

伪代码(按真实结构还原):

while (true) {  response = await streamClaudeAPI(systemPrompt, context)   # ① 流式  assistantMsg = 聚合流式片段(text / thinking / tool_use)  # ② 聚合  if (assistantMsg.tool_use.length === 0) {    return assistantMsg.text                                 # ③ 收尾  }  for (tool_use of assistantMsg.tool_use) {    permission = 检查权限(tool_use)                          # ④ 门禁    if (permission.denied) { 记录 permission_denials; continue; }    result = await executeTool(tool_use)                    # ⑤ 执行    context.push(tool_result)                                # ⑥ 回填  }}

几个关键点:

  • • 流式聚合:响应不是一次吐完,是把 text / thinking / tool_use 三种片段聚合成一条 AssistantMessage
  • • 循环推进:只要响应里还有 tool_use,循环就不停;全没了才把最后那条 text 当作 turn 输出。
  • • 权限先行:工具执行前先过权限检查,denied 会被记入 permission_denials,不会静默执行。
  • • 结果回填tool_result 作为新消息塞回上下文,下一轮模型就能"看到"执行结果。
  • • 触发压缩:上下文用到 75%–92% 容量时,触发 PreCompact hook + 信息密度评分压缩。

这里容易产生一个误解:以为工具调用是一锤子买卖——模型说"用 Bash",跑一次就完事。实际是循环:Bash 跑完,输出以 tool_result 回填,模型继续判断"下一步要不要再跑一个命令",直到它认为任务完成才会停止产出 tool_use


3,工具与权限:Tool.ts 把"能干什么"写成了契约

先说结论:Claude Code 内置 30+ 工具,但形成能力基石的只有 8 个;它们全部实现同一个 Tool 接口——输入 Schema(Zod v4 校验)、权限模型、进度状态是公共三件套。

八大核心工具:

工具
干什么
设计特点
Bash
执行 Shell 命令
"万能适配器":任何 CLI 都能调,但带 shell 注入防护与命令模式匹配(git:*npm install:* 等)
Read
读文件
最多 2000 行,超长智能截断
Write
创建 / 覆盖文件
安全检查防止误覆盖
Edit
改文件
基于 diff 的精确匹配
Grep
搜文件内容
ripgrep 后端,纯搜索不建索引
Glob
按模式找文件
按修改时间排序
Task
生成子 Agent
深度受限,上下文隔离
TodoWrite
任务进度
结构化任务清单

Bash 的"万能适配器"值得多说一句:早期版本试过用 Voyage 嵌入做语义代码搜索(RAG),但内部基准测试显示 ripgrep 表现更优,最终回到了"搜索而非索引"的策略。所以你看到的 Grep 工具背后不是向量数据库,而是一个 ripgrep 子进程——这是 Anthropic 用真金白银的 benchmark 投出来的设计选择。

权限系统是安全的核心,四档模式 + 白名单/黑名单 + 可编程回调:

  • • default:每个工具都问用户(最严格)。
  • • acceptEdits:文件编辑类(Read / Write / Edit)自动放行,其余仍问。
  • • plan:先出计划,审批后才执行(适合重构前对齐)。
  • • bypassPermissions:全部放行,仅限可信环境(需 allowDangerouslySkipPermissions)。

除了模式,SDK 还给了三个精细控制旋钮:allowedTools(自动放行白名单)、disallowedTools(直接禁掉)、canUseTool(toolName, input, ctx)(自定义回调,可以改写输入甚至拒绝):

options: {allowedTools: ["Read""Bash"],          // 这些工具自动放行disallowedTools: ["WebFetch"],            // 这些工具彻底不可见permissionMode"default",canUseToolasync (toolName, input, { signal, suggestions }) => {if (toolName === "Bash" && input.command?.includes("rm -rf")) {return { behavior"deny"message"危险命令已被拦截"interrupttrue };    }return { behavior"allow"updatedInput: input };  },}

这里容易产生一个误解:以为 allowedTools: ["Read","Write","Bash"] 是"把其他工具全禁了"。不是。它只是自动审批白名单——没列出的工具照样存在,只是每次都要过权限决策(默认弹窗,或走 canUseTool 回调)。真正"消失"是 disallowedTools 的语义。


4,扩展与安全:hooks、技能、多 Agent、上下文压缩

先说结论:Claude Code 的能力边界不是写死在代码里的——hooks 让宿主应用拦截循环、skills/plugins 让用户往里塞能力、coordinator 让多个 Agent 协作、上下文压缩让 200K 窗口经久耐用。

4.1 hooks:挂在循环上的"摄像头"和"闸门"

hooks 是 Claude Code 应用层(不是 Claude 模型本身)在循环特定节点调用的回调。完整事件包括:

  • • PreToolUse / PostToolUse / PostToolUseFailure:工具执行前后拦截,可改参数、可中止。
  • • UserPromptSubmit / Stop:用户输入提交时、本轮结束时触发。
  • • SessionStart / SessionEnd:会话启停。
  • • SubagentStart / SubagentStop:子代理启停。
  • • PreCompact / PermissionRequest:上下文压缩前 / 权限请求时。

4.2 多 Agent:AgentTool 与 coordinator

  • • 子 AgentTask / AgentTool 生成子代理,深度受限、上下文隔离,各干各的互不污染。
  • • 团队协作TeamCreateTool 支持团队级并行,coordinator/ 目录专门做编排。

4.3 扩展:skills 与 plugins

  • • 技能系统skills/ 目录定义可复用工作流,SkillTool 执行;你可以加自定义技能。
  • • 插件架构:内置 + 第三方插件都走 plugins/ 子系统统一加载。

4.4 上下文压缩:75%–92% 就要动手

  • • 触发时机:上下文用到 75%–92% 容量时触发。
  • • 信息密度评分:压缩不是盲删,先给消息算"信息密度"(代码占比越高密度越大),密度低的优先压缩。
  • • 关键消息保留:用户指令、工具结果、最终输出这些"关键消息"永远留下。

这里容易产生一个误解:以为 hooks 是给模型看的提示词。不是。hooks 是宿主进程的 JavaScript/脚本回调——PreToolUse 拦截的是"工具要不要执行",这跟模型读到的 system prompt 完全是两回事:一个是运行时控制面,一个是上下文内容。一个是门,一个是灯,别搞混。

彩蛋:源码里还藏着若干 feature flag,包括 KAIROS(自主守护模式)、PROACTIVE(主动建议)、VOICE_MODE(语音交互)、DAEMON(后台常驻)、BUDDY(一个带 18 种物种的虚拟宠物系统)。这些都没正式发布,但代码已经在了——是 Anthropic 内部对未来 Agent 形态的押注。


总结

一句话收口——Claude Code 把"agent"做成了操作系统:引擎驱动循环,工具统一契约,权限分级把关,钩子插件扩展开挂。

十六字口诀:引擎驱动循环,工具契约安全,权限分级把关,钩子扩展开挂。

你该记住的点
对应源码
一句话
循环在引擎
QueryEngine.ts
(≈46K 行)
入口只是壳,引擎才是心
工具即契约
Tool.ts
 + Zod Schema
能干什么,先声明
权限分档
default / acceptEdits / plan / bypassPermissions
按场景放宽,不一把梭
可编程拦截
canUseTool
 + hooks
规则跑在宿主侧
压缩保长寿
75%–92% 触发 + 信息密度评分
上下文是稀缺资源,要管理

底层逻辑不变——任何能"自己跑命令"的 agent,终将长出操作系统的样子:循环引擎、权限门禁、扩展生态,一个都不会少。