Claude Code 源码学习指南

面向不熟悉 TypeScript 的开发者,聚焦架构设计与工程思想。
本文基于对 claude-code/src/ 目录的完整分析整理。


目录


前言:为什么用 TypeScript 开发 CLI 工具?

直接原因:npm 是 CLI 分发的最佳渠道之一

Claude Code 通过 npm install -g @anthropic-ai/claude-code 安装。这意味着:

  • 用户只需要有 Node.js 环境(几乎所有开发者的电脑上都有)
  • npm 提供了成熟的版本管理、全局安装、依赖解析能力
  • 一行命令即可安装,跨 macOS / Linux / Windows 三平台

既然选择了 npm 作为分发渠道,用 TypeScript(编译为 JavaScript)开发就是最自然的选择——它和 npm 生态是"亲生的"关系。

深层原因:TypeScript 特别适合这类工具

优势 说明
生态丰富 终端 UI(Ink/React)、命令行解析(Commander.js)、schema 校验(Zod)等高质量库随手可用
类型安全 TypeScript 的类型系统帮助管理 50 万行代码的复杂度,LLM 返回的 JSON 可以用 Zod 做运行时校验
异步友好 async/awaitAsyncGenerator 天然适合处理 LLM 流式响应和并发工具调用
前后端同构 终端 UI 使用 React(Ink),和 Web 前端用同一套思维模型,Claude Code 还有 VS Code 插件——共享代码很方便
开发效率高 动态语言的灵活性 + 静态类型的安全性,迭代速度快

市面上主流 CLI 工具都用 TS 吗?

不是的。 CLI 工具的技术选型非常多元,取决于使用场景:

语言 代表工具 适合场景
Go Docker CLI, kubectl, gh (GitHub CLI), terraform 需要编译为单个二进制文件、追求极致性能、需要交叉编译
Rust ripgrep (rg), fd, bat, delta 系统级工具、对性能极度敏感
Python aws-cli, pip, poetry, httpie 数据/AI 领域、脚本场景、快速原型
TypeScript/JS Claude Code, Cursor, eslint, prettier, npm 自身 前端/Node.js 生态、需要丰富 UI、npm 分发
Bash/Shell oh-my-zsh, nvm 简单脚本、系统管理

选型规律:

  • 需要零依赖分发(一个二进制文件搞定) → Go / Rust
  • 需要极致性能(处理大量文件/数据) → Rust / Go
  • 面向 Node.js/前端开发者、需要丰富交互 UI → TypeScript
  • 面向数据/AI 开发者 → Python

Claude Code 选 TypeScript 的核心逻辑是:它的目标用户是开发者,分发渠道是 npm,产品形态需要复杂的终端交互 UI。在这个交叉点上,TypeScript 是最优解。


第一章:项目全景——目录结构与模块划分

Claude Code 的 src/ 目录包含约 1900 个文件、50 万行代码。下面用功能域来划分理解:

1.1 核心骨架(必须先理解的部分)

1
2
3
4
5
6
7
8
src/
├── entrypoints/cli.tsx # 真正的入口:CLI 启动分发器
├── main.tsx # 主流程:命令行解析 + 启动 REPL
├── query.ts # ★ 核心:Agent 循环(异步生成器)
├── QueryEngine.ts # Agent 循环的类封装
├── Tool.ts # ★ 核心:工具类型定义
├── tools.ts # 工具注册表
└── context.ts # 系统提示词/上下文组装

1.2 功能模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── tools/                 # 44 个内置工具的具体实现
│ ├── BashTool/ # 执行 Shell 命令(最复杂的工具)
│ ├── FileReadTool/ # 读文件
│ ├── FileEditTool/ # 编辑文件
│ ├── GrepTool/ # 搜索文件内容
│ ├── GlobTool/ # 搜索文件名
│ ├── AgentTool/ # 启动子 Agent
│ └── ...
├── commands/ # 88 个斜杠命令(/commit, /review 等)
├── services/ # 外部服务集成
│ ├── api/ # LLM API 调用(流式)
│ ├── mcp/ # MCP 协议客户端
│ ├── oauth/ # 认证
│ └── compact/ # 上下文压缩
├── screens/ # 全屏页面
│ └── REPL.tsx # 主交互界面(最大的单文件:896KB)
├── components/ # UI 组件
├── hooks/ # React Hooks
└── state/ # 应用状态管理

1.3 基础设施

1
2
3
4
5
6
7
8
9
├── ink/                   # ★ 定制版 Ink(终端 React 渲染器)
├── utils/ # 200+ 工具函数
│ ├── permissions/ # 权限系统
│ ├── settings/ # 配置加载
│ └── ...
├── types/ # TypeScript 类型定义
├── schemas/ # 数据校验 schema
├── constants/ # 常量
└── migrations/ # 配置迁移脚本

1.4 扩展能力

1
2
3
4
5
6
├── plugins/               # 插件系统
├── skills/ # 技能系统(可复用工作流)
├── coordinator/ # 多 Agent 协调模式
├── bridge/ # IDE 集成(VS Code / JetBrains)
├── voice/ # 语音输入
└── vim/ # Vim 模式

关键设计思想

关注点分离(Separation of Concerns):每个目录只负责一件事。工具的定义(Tool.ts)、注册(tools.ts)、执行(services/tools/)、权限(utils/permissions/)分布在不同模块中,彼此通过明确的接口交互。


第二章:启动流程——从敲下命令到界面就绪

当用户在终端敲下 claude 并回车,发生了什么?

2.1 四阶段启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────┐
│ Phase 1: entrypoints/cli.tsx │
│ 快速分发 —— 拦截简单请求(--version 等) │
│ 不加载任何重模块,毫秒级响应 │
│ ↓ │
│ Phase 2: main.tsx │
│ 并行预取 —— 同时启动多个子进程读取配置 │
│ 解析命令行参数(Commander.js) │
│ ↓ │
│ Phase 3: entrypoints/init.ts │
│ 初始化 —— 配置、证书、环境变量、API 预连接 │
│ ↓ │
│ Phase 4: replLauncher.tsx │
│ 渲染 React 组件树,启动交互式 REPL │
│ <App> → <REPL> → 等待用户输入 │
└─────────────────────────────────────────────────────────┘

2.2 性能优化亮点

这个启动流程体现了一个重要的工程思想:把 I/O 等待和 CPU 计算重叠执行

1
2
3
4
5
6
7
8
9
10
11
12
时间轴 ──────────────────────────────────────────→

main.tsx 入口:
├── 启动 MDM 配置读取子进程 ──────── (等待 ~65ms)
├── 启动 Keychain 密钥读取 ──────── (等待 ~65ms)
└── 同时:加载模块、解析参数 ────── (CPU ~135ms)

init.ts:
├── 启动 API 预连接 (TCP+TLS) ───── (等待 ~100ms)
└── 同时:加载其他配置 ─────────── (CPU ~50ms)

REPL 就绪,用户可以输入

学习要点:好的 CLI 工具对启动速度极其敏感。Claude Code 用了"快速路径分发"和"并行预取"两个技巧来优化冷启动时间。


第三章:Agent 循环——大脑如何思考与行动

这是整个项目最核心的部分。理解了这里,就理解了 Coding Agent 的本质。

3.1 Agent 循环是什么?

一个 Coding Agent 的工作方式可以概括为:

1
用户提问 → LLM 思考 → 调用工具 → 获取结果 → LLM 再思考 → ... → 最终回复

这个"思考→行动→观察→再思考"的循环就是 Agent Loop

3.2 核心代码结构(query.ts)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
query.ts 中的 queryLoop() 伪代码:

while (true) {
// 1. 构建请求:系统提示词 + 对话历史 + 工具定义
request = buildRequest(systemPrompt, messages, tools)

// 2. 调用 LLM(流式)
response = await callModel(request) // 流式返回

// 3. 检查响应中是否有工具调用
toolCalls = extractToolCalls(response)

if (toolCalls.length === 0) {
// LLM 认为任务完成,返回最终文本
return response
}

// 4. 执行工具调用
toolResults = await runTools(toolCalls)

// 5. 把工具结果加入对话历史
messages.push(response, toolResults)

// 6. 检查是否需要压缩上下文(防止超出 token 限制)
await autoCompactIfNeeded(messages)

// 7. 回到循环顶部,让 LLM 看到工具结果后继续思考
}

3.3 关键设计:异步生成器(AsyncGenerator)

Claude Code 用 TypeScript 的 async function* 来实现 Agent 循环:

1
export async function* query(params): AsyncGenerator<StreamEvent | Message, Terminal>

为什么用生成器而不是简单的循环?

因为循环过程中需要实时向 UI 推送中间结果

  • LLM 的流式文本(一个字一个字出现)
  • 工具调用的进度
  • 状态变化事件

生成器通过 yield 把这些中间结果"吐出来",UI 层通过 for await (const event of query(...)) 消费。这实现了计算与展示的解耦

3.4 工具执行的并发策略

1
2
3
4
5
6
7
8
LLM 返回多个工具调用时:

┌─────────────────────────────┐
│ 分类:只读 or 写入? │
│ │
│ 只读工具(Read, Grep 等) │──→ 并行执行(最多 10 个)
│ 写入工具(Edit, Bash 等) │──→ 串行执行(避免冲突)
└─────────────────────────────┘

学习要点:Agent 循环的精髓是 “让 LLM 决定下一步做什么”。代码只负责执行 LLM 的决策、管理上下文、处理边界情况(超时、重试、上下文溢出等)。


第四章:工具系统——Agent 的双手

4.1 工具的定义

每个工具是一个符合 Tool 接口的对象,核心字段:

1
2
3
4
5
6
7
8
9
Tool {
name → 标识名(如 "Bash", "Read")
description() → 告诉 LLM 这个工具干什么
prompt() → 系统提示词中关于此工具的说明
inputSchema → 输入参数的校验规则(Zod schema)
call() → 实际执行逻辑
checkPermissions() → 权限检查
isReadOnly() → 是否只读(影响并发策略)
}

4.2 工具目录结构

每个工具是一个独立目录,内部按职责拆分文件:

1
2
3
4
5
6
7
8
tools/BashTool/
├── BashTool.tsx # 工具定义与执行逻辑
├── bashPermissions.ts # 权限判断逻辑
├── bashSecurity.ts # 安全分析(命令注入检测等)
├── prompt.ts # 提示词文本
├── UI.tsx # 终端渲染组件
├── pathValidation.ts # 路径校验
└── readOnlyValidation.ts # 只读命令判断

这种"按工具独立成目录"的组织方式让每个工具像一个"微模块",各自独立发展,互不干扰。

4.3 工具注册与发现

1
2
3
4
5
tools.ts
├── getAllBaseTools() → 返回所有工具的主列表
├── getTools() → 过滤掉被禁用/被拒绝的工具
└── assembleToolPool() → 合并内置工具 + MCP 外部工具
(排序固定,利于 LLM 提示词缓存)

4.4 MCP 工具的动态接入

Claude Code 支持 MCP(Model Context Protocol)协议,可以动态接入外部工具:

1
2
3
4
5
6
7
内置工具:  Bash, Read, Edit, Grep, Glob, Agent, ...
+
MCP 工具: mcp__github__create_pr, mcp__jira__list_issues, ...

统一的工具池(Tool Pool)

传给 LLM 做选择

MCP 工具的命名规则是 mcp__<服务器名>__<工具名>,通过统一的 Tool 接口与内置工具一视同仁。


第五章:权限与安全——信任的边界

这是 Claude Code 工程上最复杂的子系统之一,也是 Coding Agent 产品的生命线。

5.1 权限模式

模式 行为 场景
default 危险操作前询问用户 日常使用
plan 只读工具自动通过,写入需确认 规划阶段
acceptEdits 文件编辑自动通过,Shell 仍询问 信任文件修改
bypassPermissions 全部自动通过 完全信任(危险)
auto AI 分类器自动判断是否安全 实验性功能

5.2 权限判断流程

1
2
3
4
5
6
7
8
9
10
11
12
13
工具调用请求

① 全局拒绝规则检查 ──→ 匹配 → 拒绝
↓ 不匹配
② 全局允许规则检查 ──→ 匹配 → 放行
↓ 不匹配
③ 权限模式判断 ────→ bypass → 放行
↓ plan → 只读放行/写入询问
④ 工具特定权限检查

⑤ 向用户显示权限确认对话框

用户决定:允许 / 拒绝 / 总是允许

5.3 BashTool 的安全分析

BashTool 是最危险的工具(可以执行任意 Shell 命令),因此有最复杂的安全机制:

  • 命令解析:分析 Shell 命令的 AST,识别危险操作
  • 路径校验:确保不会操作工作目录之外的文件
  • 只读判断:识别 git statusls 等只读命令可以自动放行
  • 注入检测:防止通过巧妙构造的命令绕过安全检查

学习要点:在 Agent 系统中,权限不是可选的附加功能,而是核心架构的一部分。Claude Code 的权限系统贯穿了从工具定义到 UI 展示的整条链路。


第六章:终端 UI——在命令行里做"前端"

6.1 技术选型:Ink(React for Terminal)

Claude Code 在终端中使用 React 来构建 UI,这听起来很反直觉,但非常巧妙:

1
2
Web 应用:   React → ReactDOM → 浏览器 DOM → 像素
Claude Code: React → Ink → 终端虚拟 DOM → ANSI 字符

Ink 是一个把 React 组件渲染到终端的库。Claude Code 甚至自己 fork 了一份 Ink(在 src/ink/ 目录),做了大量定制:

  • 自定义 React 协调器(reconciler)
  • 基于 Yoga 的 Flexbox 布局(是的,终端里也能用 Flexbox)
  • 文本选择支持
  • 点击事件处理
  • 高效的差异更新(只重绘变化的部分)

6.2 组件树结构

1
2
3
4
5
6
7
8
<App>                          # 全局状态 Provider
└── <REPL> # 主交互界面(896KB,最大的单文件)
├── <PromptInput> # 用户输入框(支持自动补全)
├── <Messages> # 对话消息列表(虚拟滚动)
├── <PermissionRequest> # 权限确认对话框
├── <Spinner> # 加载动画
├── <StatusLine> # 底部状态栏
└── <HighlightedCode> # 语法高亮代码块

6.3 为什么 REPL.tsx 有 896KB?

这个文件管理了整个交互界面的所有状态和逻辑,包括:

  • 消息展示与虚拟滚动
  • 用户输入与自动补全
  • 工具权限弹窗
  • LLM 流式响应的渲染
  • 会话管理(恢复、后台、导出)
  • IDE 桥接
  • 语音输入
  • Vim 模式
  • 快捷键处理

这是一个典型的"上帝组件"——虽然不一定是最佳实践,但说明了交互式 REPL 的复杂度远超表面看起来的样子。


第七章:配置系统——灵活性的来源

7.1 多层配置合并

Claude Code 的配置来自多个来源,按优先级从高到低合并:

1
2
3
4
5
6
7
8
最高优先级
↑ 组织策略(policySettings) ← 企业管理员强制设定
│ 命令行标志(flagSettings) ← 本次运行指定
│ 本地配置(.claude/settings.local.json) ← 不提交到 Git
│ 项目配置(.claude/settings.json) ← 提交到 Git,团队共享
│ 用户配置(~/.claude/settings.json) ← 个人全局偏好
↓ 远程托管配置(managed) ← 服务端下发
最低优先级

7.2 CLAUDE.md 记忆系统

1
2
3
项目根目录/CLAUDE.md       ← 项目级指令(提交到 Git)
~/.claude/CLAUDE.md ← 用户级指令(个人偏好)
.claude/settings.local.json ← 本地覆盖

CLAUDE.md 文件会被自动发现并注入到系统提示词中,让 Agent "记住"项目特有的规范和偏好。


第八章:扩展机制——插件、技能与多 Agent

8.1 插件系统(Plugins)

插件可以提供:

  • 额外的工具
  • 额外的斜杠命令
  • Hook 处理器
  • MCP 服务器配置
1
2
3
plugins/
├── builtinPlugins.ts # 内置插件加载
└── bundled/ # 打包在内的插件

8.2 技能系统(Skills)

技能是"可复用的工作流模板",比插件更轻量:

1
2
/commit    → 触发 commit 技能 → 自动分析变更、生成提交信息
/review-pr → 触发 review 技能 → 自动审查 PR

8.3 多 Agent 协调

1
2
3
4
协调者 Agent(Coordinator)
├── 创建工作者 Agent A → 处理子任务 1
├── 创建工作者 Agent B → 处理子任务 2
└── 汇总结果

工作者通过 AgentTool(启动子 Agent)和 SendMessageTool(消息传递)与协调者交互。工作者的权限通过 bubble 模式向上委托给协调者决策。


第九章:工程实践——值得学习的设计模式

9.1 快速路径分发(Fast Path)

entrypoints/cli.tsx 中,像 --version 这样的简单请求在加载任何模块之前就被拦截返回。这避免了为一个简单的版本号查询加载 50 万行代码。

思想:区分"轻量请求"和"重量请求",为轻量请求提供捷径。

9.2 并行预取(Parallel Prefetch)

启动时,配置读取、密钥读取、API 预连接等 I/O 操作并行发起,不等前一个完成再开始下一个。

思想:I/O 等待时间可以与 CPU 计算重叠,充分利用等待时间。

9.3 异步生成器实现流式处理

query()async function* 实现,允许 Agent 循环一边执行一边向 UI 推送结果。

思想:生成器是"生产者-消费者"模式的优雅实现,比回调和事件监听更易读。

9.4 依赖注入提高可测试性

query/deps.ts 将外部依赖(API 调用、压缩等)抽象为接口,测试时可以替换为 mock 实现:

1
2
生产环境: deps.callModel = 真实的 API 调用
测试环境: deps.callModel = 返回预设响应的 mock

思想:不直接依赖具体实现,依赖抽象接口。

9.5 工具即数据(Tool as Data)

每个工具不是一个"类",而是一个符合 Tool 接口的"数据对象"。这让工具的注册、过滤、排序、序列化都变得简单。

思想:优先用数据描述行为,而不是用继承体系。

9.6 深度不可变(DeepImmutable)

权限上下文等关键数据结构使用 DeepImmutable<T> 类型,编译期就防止意外修改。

思想:重要状态应该是不可变的,修改状态应该是显式的。

9.7 构建时特性裁剪(Dead Code Elimination)

通过 import { feature } from 'bun:bundle',在构建时决定哪些功能包含在最终产物中。不需要的功能代码在编译阶段就被删除,不会增加安装包体积。

思想:同一份代码可以产出不同功能集的产品(内部版 vs 公开版)。


附录:关于 CLI 工具的技术选型

各语言的典型使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                性能需求


Rust │ Go
(ripgrep,│ (docker, kubectl,
fd, bat)│ gh, terraform)

───────────────────┼───────────────────

Python │ TypeScript
(aws-cli, │ (Claude Code, eslint,
poetry) │ prettier, Cursor)


脚本/AI 生态 Web/Node 生态

选型决策树

1
2
3
4
5
6
7
8
9
10
11
12
你的 CLI 需要:
├── 编译为单个无依赖二进制?
│ ├── 是 → Go(简单)或 Rust(极致性能)
│ └── 否 ↓
├── 面向 Node.js/前端开发者?
│ ├── 是 → TypeScript(npm 分发)
│ └── 否 ↓
├── 面向数据/AI 开发者?
│ ├── 是 → Python(pip 分发)
│ └── 否 ↓
└── 简单的系统管理脚本?
└── 是 → Bash/Shell

为什么 AI Coding Agent 多用 TypeScript?

当前主流的 AI Coding Agent CLI 工具(如 Claude Code、Cursor 等)多选择 TypeScript,原因包括:

  1. 目标用户群体:开发者群体普遍有 Node.js 环境
  2. 终端 UI 需求:Ink (React) 提供了最成熟的终端交互框架
  3. 流式处理:JS 的异步模型天然适合 LLM 流式响应
  4. 快速迭代:AI 领域变化极快,TS 的开发效率优势明显
  5. 全栈共享:CLI + VS Code 插件 + Web 界面可以共享大量代码

但这不是唯一选择。例如 aider(另一个知名 Coding Agent)就是 Python 写的,因为它更偏向 Python 开发者群体。技术选型永远服务于产品目标和用户群体。


推荐学习路径

如果你想深入理解这个项目,建议按以下顺序阅读:

顺序 文件/目录 理解目标
1 entrypoints/cli.tsx 整体入口,理解启动分发
2 query.ts Agent 循环,理解核心机制
3 Tool.ts + tools.ts 工具系统的抽象设计
4 tools/FileReadTool/ 一个简单工具的完整实现
5 tools/BashTool/ 最复杂的工具,理解安全设计
6 utils/permissions/ 权限系统
7 services/api/ LLM 调用如何实现
8 screens/REPL.tsx UI 层如何消费 Agent 循环的输出
9 services/mcp/ 外部工具扩展机制
10 coordinator/ 多 Agent 架构

不需要逐行阅读 TypeScript 代码。重点关注文件之间的调用关系数据如何流动关键接口的定义。架构理解比语法理解更重要。