# Claude Code 工作原理科普·深入版：一段 Prompt 的奇幻漂流

> **本文基于 `claude-code` 源码（约 2026 年 3 月版本）的实际实现写成。**
> **适合读者**：计算机初学者——你不需要会写代码，只需要知道"程序就是一步一步执行指令"。
> **阅读建议**：文中包含大量 mermaid 图表，请配合支持 mermaid 渲染的阅读器（如 VS Code + Markdown Preview Mermaid Support 插件、Typora、Obsidian）阅读；所有关键结论都标注了 `文件:行号`，可顺藤摸瓜去 `./claude-code/src/` 查证。
>
> 与入门版相比，本版每一章都深入到真实源码：直接引用代码片段并逐行注释、讲清"为什么这样设计"的工程取舍、每个机制配图、每类场景举多个不同命运的例子。

---

> 🗺️ **配套架构图**：本项目在[**七大 Agent 架构图库**](架构图库.html#ch2)里有一张专门的图——**一次回合的完整时序（权限是横切闸门，上下文每轮重组）**。
> 图库的每张图都先写清「回答什么问题」和「承重墙论点」，并经三轮审阅与渲染验收。

## 目录

1. [先搞懂概念：什么是 AI Agent？](#一先搞懂概念什么是-ai-agent)
2. [全景图：五层架构与一次请求的数据流](#二全景图五层架构与一次请求的数据流)
3. [旅程开始：启动时软件做了什么](#三旅程开始启动时软件做了什么)
4. [按下回车的那一刻：输入的分流之旅](#四按下回车的那一刻输入的分流之旅)
5. [打包行李：发往 AI 的请求是怎么拼出来的](#五打包行李发往-ai-的请求是怎么拼出来的)
6. [心脏地带：Agent 主循环（最重要的一章）](#六心脏地带agent-主循环最重要的一章)
7. [AI 的"手"：工具系统与权限闸门](#七ai的手工具系统与权限闸门)
8. [工具图鉴：AI 的 40 件兵器，逐一上手](#八工具图鉴ai-的-40-件兵器逐一上手)
9. [斜杠命令图鉴：60+ 个快捷指令的工作原理](#九斜杠命令图鉴60-个快捷指令的工作原理)
10. [生态组件：插件、技能、MCP 各是什么](#十生态组件插件技能mcp-各是什么)
11. [功能特性拆解：effort、登录、模型切换……](#十一功能特性拆解effort登录模型切换)
12. [记忆与遗忘：上下文压缩](#十二记忆与遗忘上下文压缩)
13. [分身术与急刹车：子 Agent 与中断机制](#十三分身术与急刹车子-agent-与中断机制)
14. [总流程图：一图看完全程](#十四总流程图一图看完全程)
15. [附录：源码导览索引](#附录源码导览索引)

---

## 一、先搞懂概念：什么是 AI Agent？

### 1.1 大模型本身只会"说话"——而且是无记忆的说话

我们先从两个常见的误解说起。

**误解一：AI 是什么都会做的智能程序。** 其实大语言模型（LLM）本身只会做一件事：读一段文字，然后接着往下写文字。你可以把它想象成一个被关在房间里的超级博学的人——他读过全世界的书，但没有手、没有眼睛、没有电话，你和他交流的唯一方式是从门缝递纸条。

**误解二：AI 记得你说过的话。** 其实模型**没有任何记忆**。每次你发消息，软件都要把从第一句话开始的**全部聊天记录**重新发一遍。所谓"它记得"，是软件替它记的。这一点后面会反复用到——它是理解"为什么会话越长越慢越贵""为什么需要压缩"的钥匙。

这两个误解指向同一个结论：光有大模型，它没法帮你"改代码文件"——它只能告诉你"你应该把第 3 行改成 xxx"，改文件这个动作它自己做不了。

### 1.2 Agent = 大脑 + 手 + 循环

**Agent（智能体）** 就是给这个"房间里的人"配上手和眼睛的一套软件系统：

| 比喻 | 在 Claude Code 里对应什么 | 源码位置 |
|---|---|---|
| 🧠 大脑 | Claude 大模型（API 远程调用） | `src/services/api/claude.ts` |
| ✋ 手 | **工具（Tools）**：读写文件、执行命令、搜索 | `src/tools/`（约 40 个） |
| 👀 眼睛 | 工具执行后返回的结果（`tool_result`） | `src/services/tools/toolExecution.ts` |
| 🔁 神经系统 | **主循环**：把结果喂回大脑，让它决定下一步 | `src/query.ts` 的 `queryLoop` |
| 🛡️ 保安 | 权限系统：危险动作先征得你同意 | `src/utils/permissions/` |

Agent 的核心思想一句话就能说完：

> **模型说话 → 软件替它动手 → 把动手的结果再告诉模型 → 模型继续说 → 直到事情办完。**

### 1.3 模型"想用工具"，在数据里到底长什么样？

这是初学者最容易觉得"玄"的地方，我们用数据把它彻底拆穿。模型不会真的打电话，它只是在回复的文字流里生成一个特殊结构，叫 **`tool_use` 块**，大致是：

```json
{
  "type": "tool_use",        // 块的类型：这是一次工具调用
  "id": "toolu_01ABC...",    // 这次调用的唯一编号（后面配对要用）
  "name": "Bash",            // 要用哪个工具
  "input": { "command": "ls src" }  // 给工具的参数
}
```

软件收到这个块，就知道"模型要动手了"，于是替它执行。执行结果再包成另一个特殊结构（**`tool_result` 块**）发回去：

```json
{
  "type": "tool_result",
  "tool_use_id": "toolu_01ABC...",   // 注意：用 id 配对，标明这是哪次调用的结果
  "content": "a.ts\nb.ts\nc.ts"      // 真实的命令输出
}
```

整个 Agent 循环，说穿了就是软件在当"翻译官 + 跑腿"：**把模型的文字意图翻译成真实操作，再把真实操作的结果翻译回文字。** 而那个 `id` 配对机制，后面第十三章会讲到一个由它引出的关键设计（"孤儿工具结果必须补假回信"）。

### 1.4 一个最小例子

假设你说："*看看 src 目录里有什么文件，然后告诉我哪个文件最大*"。软件内部实际发生的对话是这样的：

```mermaid
sequenceDiagram
    participant 你
    participant 软件 as Claude Code 软件
    participant 模型 as Claude 大模型

    你->>软件: 看看 src 里有什么文件，哪个最大？
    软件->>模型: 【打包】员工手册+git状态+你的问题+工具清单
    模型-->>软件: tool_use: Bash("ls src")
    软件->>软件: 真的去执行 ls src（动手！）
    软件->>模型: tool_result: a.ts b.ts c.ts
    模型-->>软件: tool_use: Bash("du -h src/*")
    软件->>软件: 执行 du -h src/*
    软件->>模型: tool_result: a.ts 2K, b.ts 8K, c.ts 1K
    模型-->>软件: 纯文字（无 tool_use）：b.ts 最大，8KB
    软件->>你: 📄 b.ts 最大（8KB）
```

注意看：模型说了三次话，前两次都是 `tool_use`，第三次没有 `tool_use` 了——**"没有工具调用"就是循环的停止信号**。剩下的所有复杂性，都是为了让这个循环更安全、更快、更省 token。

---

## 二、全景图：五层架构与一次请求的数据流

在深入细节之前，先建立一张地图。Claude Code 从外到内可以分成五层：

```mermaid
flowchart TB
    subgraph L1["🖥️ 第 1 层：界面层（终端 UI）"]
        UI["输入框 / 文本渲染 / 权限弹窗<br/><i>src/components · src/screens/REPL.tsx · src/ink</i>"]
    end
    subgraph L2["🚦 第 2 层：输入分流层"]
        ROUTER["判断输入是斜杠命令 / !shell / 普通提问<br/><i>src/utils/handlePromptSubmit.ts · processUserInput.ts</i>"]
    end
    subgraph L3["📦 第 3 层：上下文组装层"]
        CTX["系统提示词 + CLAUDE.md + git 状态<br/><i>src/constants/prompts.ts · src/context.ts · src/utils/api.ts</i>"]
    end
    subgraph L4["🔁 第 4 层：Agent 主循环（心脏）"]
        LOOP["请求模型 → 执行工具 → 追加结果 → 再请求<br/><i>src/query.ts · src/services/tools/</i>"]
    end
    subgraph L5["🌐 第 5 层：API 通信层"]
        API["流式 HTTP / SSE 解析 / 重试 / 中断<br/><i>src/services/api/claude.ts</i>"]
    end

    UI --> ROUTER --> CTX --> LOOP --> API
    LOOP -.->|调用| TOOLS["🔧 工具箱：Bash / Read / Write / Edit / Grep ...<br/><i>src/tools/</i>"]
    API -.->|HTTPS| CLAUDE["☁️ Anthropic 服务器上的 Claude 模型"]
```

- **第 1 层（界面）**：你在终端里看到的输入框、打字效果、"是否允许执行此命令？"的弹窗，都是用 React 渲染在终端里的（一个叫 Ink 的库，魔改版在 `src/ink/`）。用 React 写终端界面是个有趣的选择：开发者可以复用成熟的组件思维来管理复杂界面状态。
- **第 2 层（分流）**：你敲回车后，软件先判断这段话是发给 AI 的，还是本地命令。这一层的规则朴素得令人意外（第四章细讲）。
- **第 3 层（打包）**：确认要发给 AI 后，把系统提示词、记忆文件、git 状态等"背景资料"打包。这一层的核心矛盾是"给模型的信息越多越好"与"token 又贵又慢"之间的平衡（第五章细讲）。
- **第 4 层（循环）**：真正的心脏。全软件最精密的工程都堆在这里（第六、十二、十三章细讲）。
- **第 5 层（通信）**：负责和 Anthropic 服务器说话。关键词是"流式"——服务器写一点传一点，所以你看到回答是一个字一个字"打"出来的。

一次完整的请求，数据是这样在各层之间流动的：

```mermaid
sequenceDiagram
    participant UI as ① 界面层
    participant RT as ② 分流层
    participant CX as ③ 组装层
    participant LP as ④ 主循环
    participant API as ⑤ 通信层
    participant CL as ☁️ 模型

    UI->>RT: 回车，交出一整段文本
    RT->>RT: 空?exit?!开头?/开头?
    RT->>CX: 判定"发给 AI"
    CX->>CX: 取缓存的系统提示词/CLAUDE.md/git状态
    CX->>LP: 完整消息数组 + 工具清单
    loop 主循环（可能转很多圈）
        LP->>API: 流式请求
        API->>CL: HTTPS (stream: true)
        CL-->>API: SSE 事件流（逐字/逐块）
        API-->>LP: 组装好的 text / tool_use 块
        LP->>LP: 有 tool_use 就执行工具，结果追加进历史
    end
    LP-->>UI: 最终答案（无 tool_use 的那次回复）
```

接下来，跟着一段 prompt 从出生到答案诞生，一层一层走一遍。

---

## 三、旅程开始：启动时软件做了什么

你在终端敲下 `claude`、按下回车，到输入框出现之间，软件悄悄做了很多事。先看全流程：

```mermaid
flowchart LR
    A["敲下 claude"] --> B["分拣员 cli.tsx<br/>特殊参数走快速通道"]
    B --> C["main.tsx<br/>解析命令行参数"]
    C --> D["init.ts 全局初始化（只做一次）<br/>配置/安全环境变量/CA证书/预连API"]
    D --> E["setup.ts 会话初始化<br/>定工作目录/hooks 配置快照"]
    E --> F{"目录信任对话框<br/>❓信任此文件夹吗？"}
    F -->|信任| G["后台预热上下文<br/>预读 git 状态 + CLAUDE.md"]
    G --> H["渲染交互界面<br/>输入框出现！"]
    F -->|拒绝| I["退出"]
```

### 3.1 分拣员：`--version` 为什么快得像闪电

程序入口 `src/entrypoints/cli.tsx` 的 `main()` 像一个快递分拣员。看这段真实源码（约 `cli.tsx:27-44`）：

```ts
/**
 * Bootstrap entrypoint - checks for special flags before loading the full CLI.
 * All imports are dynamic to minimize module evaluation for fast paths.
 * Fast-path for --version has zero imports beyond this file.
 */
async function main(): Promise<void> {
  const args = process.argv.slice(2);

  // Fast-path for --version/-v: zero module loading needed
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
    // MACRO.VERSION is inlined at build time
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;   // ← 打印完直接 return，庞大的对话系统一行都没加载
  }
  // ……其他快速路径（--dump-system-prompt 等）……
  // 默认路径：await import('../main.js') 动态加载主程序
}
```

逐行注释：文件头的注释直接点破了设计意图——"`--version` 快速通道除了本文件外**零 import**"。主程序有上千个模块，全部加载要几百毫秒；而 `console.log` 一个版本号只要几毫秒。所以分拣员先看参数：是 `--version` 这种简单请求，打印完立刻 `return`；只有默认路径才用 `await import('../main.js')` **动态加载**主程序。这就是为什么 `claude --version` 秒回，而 `claude` 启动需要一点等待。

**为什么这样设计？** 一个命令行工具的"体感速度"很大程度上由最简单命令的响应速度决定（想想看 `git --version` 如果卡 2 秒你会有多烦躁）。把重型模块全部改成动态 import，用"启动时少做事"换取"常用路径飞快"，是 CLI 工具的经典取舍。

### 3.2 全局初始化：只做一次的"开业准备"

进入主程序后，会执行 `src/entrypoints/init.ts` 的 `init()`。注意它的定义方式（`init.ts:57`）：

```ts
export const init = memoize(async (): Promise<void> => {
  // 读配置、设置安全相关环境变量、处理企业 CA 证书……
  // （init.ts:159）preconnectAnthropicApi()
})
```

`memoize`（记忆化）意味着：不管代码里有多少个地方调用 `init()`，**函数体只真正执行一次**，之后的调用直接返回第一次的结果。初始化里有一件特别的事——`preconnectAnthropicApi()`：提前和 Anthropic 服务器做一次 TLS 网络握手。这就像打电话先拨号放着，等你真的说话时不用等拨号音，能省下 100~200 毫秒。**为什么放在初始化而不是第一次请求时？** 因为握手可以和你"看信任对话框、敲第一个问题"的时间并行，属于白捡的加速。

### 3.3 会话初始化：hooks 的"防暗改"快照

`src/setup.ts` 做会话级初始化：确定当前工作目录（`setCwd`）、处理 `--worktree` 等参数。其中一个安全细节：用户配置的 **hooks（钩子脚本，第七章细讲）会在这里拍一张"快照"**。为什么？hooks 是"在某些时刻自动执行你电脑上的 shell 命令"的配置，威力很大；如果会话进行中有人偷偷改了配置文件（比如某个恶意脚本），快照机制能保证本会话一直用启动时那份你确认过的配置，防会话中被暗改。

### 3.4 信任对话框：一道真正的安全边界

第一次在某个文件夹运行，会弹"你信任这个目录吗？"（`src/interactiveHelpers.tsx` 的 `showSetupScreens`）。这不是走形式的欢迎页，而是安全边界：项目目录里可能藏着别人写的配置（hooks、CLAUDE.md），**恶意仓库可以借此让你的电脑执行危险命令**。你点"信任"之后，这些项目级配置才会生效。

而且信任一通过，软件立刻**在后台**预取 `getSystemContext()`（git 状态）和 `getUserContext()`（CLAUDE.md），这两个函数都是 memoize 缓存的——预取等于"提前把缓存烧热"，你第一个问题的响应因此更快。这和 3.2 的预连 API 是同一个思路：**能提前做的准备工作，绝不等到用户开口才做。**

> 🔍 **源码指路**：`src/entrypoints/cli.tsx:27-44`（分拣员）｜`src/entrypoints/init.ts:57,159`（memoized init 与预连）｜`src/setup.ts`（hooks 快照）｜`src/interactiveHelpers.tsx`（信任对话框与预热）

---

## 四、按下回车的那一刻：输入的分流之旅

输入框出现了。你打了一段字，按下回车。从这一秒到"请求发出"，中间隔着一条比你想象长得多的链路。

### 4.1 从按键到文本：一段字节到字符的旅程

你在键盘上敲的每个键，都要经历一段旅程：

```mermaid
flowchart LR
    A["键盘按键"] --> B["终端把按键变成<br/>原始字节流（stdin）"]
    B --> C["App.tsx parseMultipleKeypresses<br/>ANSI 转义序列状态机<br/><i>把字节解析成'结构化按键'</i>"]
    C --> D["useInput hook 分发"]
    D --> E["useTextInput.ts<br/>文本状态机：更新内容与光标"]
    E --> F{"是回车键？"}
    F -->|普通字符| E
    F -->|回车| G["onSubmit(完整文本)<br/><b>文本正式离开输入框</b>"]
```

第 3 步值得解释：终端里方向键、回车、Ctrl 组合键都不是普通字符，而是一串以 ESC 开头的"转义序列"（比如按 ↑ 实际收到 3 个字节 `ESC [ A`）。所以软件需要一个**状态机**把这些字节流还原成"用户按了上箭头"这样的结构化事件。

回车键的处理在 `src/hooks/useTextInput.ts:247` 的 `handleEnter`：

```ts
function handleEnter(key: Key) {
  if (multiline && cursor.offset > 0 &&
      cursor.text[cursor.offset - 1] === '\\') {
    // 行尾是反斜杠 \ → 用户想换行而不是提交
    return cursor.backspace().insert('\n')
  }
  if (key.meta || key.shift) {
    return cursor.insert('\n')   // Meta+Enter / Shift+Enter → 换行
  }
  // Apple Terminal 不支持自定义 Shift+Enter，用系统级修饰键检测兜底
  if (env.terminal === 'Apple_Terminal' && isModifierPressed('shift')) {
    return cursor.insert('\n')
  }
  onSubmit?.(originalValue)      // 以上都不是 → 才是真正的提交
}
```

逐行注释：回车**不一定等于提交**。前三道分支都在处理"用户其实想换行"的情形（行尾 `\`、`Shift+Enter`、苹果终端的特例），全部排除后才调用 `onSubmit`。这就是为什么你在输入框里能用 `\` 加回车写多行文本。

### 4.2 UI 把关：onSubmit 之前还有一道筛子

文本离开 `useTextInput` 后，还要过 `PromptInput.tsx`（约 `:984`）的局部 `onSubmit` 这一关，它负责几件"UI 层才能判断"的事：

- **页脚对话框打开时不提交**（你正在看帮助页，回车是给对话框的）；
- **幽灵文本采纳**：输入框里那行灰色的"猜你想输入"提示，按回车可能是在采纳它而不是提交；
- **@团队成员直发**：多 agent 协作时 `@名字 内容` 直接路由给对应成员；
- **空输入直接丢弃**。

全部通过，才轮到 `src/screens/REPL.tsx:3142` 的 `onSubmit` 接管——从这里开始，才是真正的"分拣台"。

### 4.3 分拣台：纯字符串匹配，零 AI 参与

很多人以为分拣台会"智能理解"你的意图，其实它只用几条**写死的字符串规则**，全部是最朴素的比较。

**规则 1：看第一个字符**（`src/components/PromptInput/inputModes.ts:16`）：

```ts
export function getModeFromInput(input: string): HistoryMode {
  if (input.startsWith('!')) {   // 首字符是 ! → bash 模式
    return 'bash'
  }
  return 'prompt'                // 否则 → 候选发给 AI
}
```

斜杠命令同理：`processUserInput.ts` 里的 `inputString.startsWith('/')`。

**规则 2：空白输入直接丢弃**（`src/utils/handlePromptSubmit.ts:188`）：`if (input.trim() === '') return`——你只敲了几个空格就回车，什么都不会发生。

**规则 3：退出词是一份写死的名单**（`handlePromptSubmit.ts:196`）：

```ts
if (['exit', 'quit', ':q', ':q!', ':wq', ':wq!'].includes(input.trim())) {
  // 改写成 /exit 命令，走正常的退出流程（还会弹反馈对话框）
}
```

注意是 `.includes()`——必须**精确等于**名单中的某个词。你说"帮我退出这个死循环"这种自然语言完全不匹配，会照常发给 AI。（`:q`、`:wq` 是照顾 Vim 用户肌肉记忆的彩蛋。）

**规则 4：斜杠命令是查字典，也是精确匹配**（`src/commands.ts:688`）：

```ts
export function findCommand(commandName: string, commands: Command[]) {
  return commands.find(
    _ =>
      _.name === commandName ||            // 名字精确相等
      getCommandName(_) === commandName || // 或全名精确相等
      _.aliases?.includes(commandName),    // 或命中别名列表
  )
}
```

查不到就老老实实回 `Unknown command: xxx`，绝不会"猜"你是不是想输别的命令。

**为什么设计得这么"笨"？** 这是刻意的工程取舍：

1. **确定性**：你的输入该不该发给 AI，涉及隐私和费用，必须 100% 可预测。若用"智能分类"判断，一旦误判，你的本地私语可能就被上传了；
2. **快**：字符串比较是纳秒级，零延迟、零 token 消耗；
3. **可调试**：规则写在代码里，行为完全可复现。

真正"理解"你输入的环节，发生在分拣**之后**。分拣台只是个按章办事的门卫：不认识人，只看证件。

### 4.4 七种输入，七种命运

把规则串起来，看七个真实输入分别走向哪里（这是本章最重要的一张图）：

```mermaid
flowchart TD
    INPUT["你敲下的文本"] --> Q1{"trim 后为空？"}
    Q1 -->|如只敲了空格| DROP["🗑️ 丢弃<br/><i>handlePromptSubmit.ts:188</i>"]
    Q1 -->|否| Q2{"精确等于 exit/quit/:q/:q!/:wq/:wq!？"}
    Q2 -->|是| EXIT["本地执行 /exit，退出程序<br/><b>不发 AI</b>"]
    Q2 -->|否| Q3{"! 开头？"}
    Q3 -->|是| BASH["💻 bash 模式：本地直接执行 shell<br/>输出只给你看<br/><b>不发 AI（shouldQuery:false）</b>"]
    Q3 -->|否| Q4{"/ 开头？"}
    Q4 -->|是| CMD{"findCommand 查命令表"}
    CMD -->|local-jsx 型<br/>如 /config| LOCALUI["⚙️ 打开本地界面<br/><b>不发 AI</b>"]
    CMD -->|local 型<br/>如 /clear| LOCAL["⚙️ 本地执行（清空对话）<br/><b>不发 AI</b>"]
    CMD -->|prompt 型<br/>如 /commit、技能| PROMPT["📝 展开成一段预制提示词<br/><b>发给 AI</b>"]
    CMD -->|查无此令| UNKNOWN["提示 Unknown command<br/><b>不发 AI</b>"]
    Q4 -->|否| Q5{"AI 正在忙吗？<br/><i>queryGuard.isActive</i>"}
    Q5 -->|在忙| QUEUE["📥 进 messageQueueManager 排队<br/>当前回合结束后再处理"]
    Q5 -->|空闲| TEXT["✉️ 包装成用户消息<br/><b>发给 AI（shouldQuery:true）</b>"]
```

逐个例子过一遍：

1. **`!ls -la`** → bash 模式。命令直接在你电脑上执行，输出显示给你看，`shouldQuery: false`（`processUserInput.ts:206`）——**一字节都不会发给模型**。适合你自己快速跑个命令的场景。
2. **`/clear`** → local 型斜杠命令：本地清空对话历史，不发 AI。这就是为什么 `/clear` 不消耗 API 额度。
3. **`/config`** → local-jsx 型：在终端里渲染一个设置界面让你点选，也不发 AI。
4. **`/commit`** → prompt 型：软件把命令展开成一段预制提示词（大意是"看看 git 现状，帮我写提交信息并提交"），**这段展开后的文字**才发给 AI。技能（Skill）也走这条路。
5. **`帮我改 bug`** → 普通提问：包装成用户消息发给 AI，`shouldQuery: true`。
6. **AI 正忙时你发的消息** → 进 `messageQueueManager` 队列。等当前回合结束，队列里的消息会被取出重新走一遍分拣流程。
7. **`exit` 或 `:q`** → 本地退出。注意它改写成 `/exit` 走正常命令流程，而不是粗暴的 `process.exit`——这样退出前还能做清理、弹反馈对话框。

另外两个容易困惑的点：

- **按 ↑ 键调出的历史命令会发给 AI 吗？** 不会。那份历史存在本地 `~/.claude/history.jsonl`（`src/history.ts`），只服务"上箭头召回"，和发给模型的对话历史是两码事。
- **发出之前还有一道用户自装的安检门：UserPromptSubmit 钩子**（`processUserInput.ts:174-264`）。如果你在配置里写了钩子脚本，它可以在这里拦截你的输入、改写它、或追加额外资料。第七章会细讲钩子机制。

### 4.5 附件：@文件 是怎么塞进去的

如果输入里写了 `@src/main.ts`，或者你的 IDE 里有选中的代码，这些会在这个阶段被 `src/utils/attachments.ts` 的 `getAttachmentMessages` 收集成"附件消息"，跟在你的问题后面一起发出。粘贴的图片也是在这里被压缩、转成模型能读的图片块。技能清单附件（第五章会讲的"渐进披露"）也走这个通道。

> 🔍 **源码指路**：`src/hooks/useTextInput.ts:247`（回车处理）｜`src/components/PromptInput/PromptInput.tsx:984`（UI 把关）｜`src/screens/REPL.tsx:3142`（REPL onSubmit）｜`src/utils/handlePromptSubmit.ts:188,196`（空白丢弃、退出名单）｜`src/components/PromptInput/inputModes.ts:16`（`!` 判断）｜`src/commands.ts:688`（findCommand）｜`src/utils/processUserInput/processUserInput.ts:174-264,446`（UserPromptSubmit 钩子、bash 模式）｜`src/utils/attachments.ts`（附件收集）

---

## 五、打包行李：发往 AI 的请求是怎么拼出来的

你的问题被确认"要发给 AI"了。你以为发过去的只有"帮我看看这个函数有没有 bug"这一句话？其实每次请求都像寄出一个大包裹：

```mermaid
flowchart TB
    subgraph PACKAGE["📦 发往 Anthropic API 的一个完整请求"]
        direction TB
        SYS["1️⃣ system 字段：系统提示词<br/>静态段（角色/规范/工具说明）+ 动态段（技能清单/环境信息）<br/><b>末尾追加</b> gitStatus"]
        MSG0["2️⃣ 对话第一条 user 消息（isMeta 隐身）<br/>&lt;system-reminder&gt; 包裹的 CLAUDE.md + 今天日期"]
        HISTORY["3️⃣ 对话历史<br/>你说过的话 + AI 每次回复 + 每次工具结果，原封不动全带"]
        NEW["4️⃣ 你的新消息 + @附件/图片"]
        TOOLS["5️⃣ tools 参数：约 40 个工具的说明书<br/>名字 + 用途描述 + 参数 JSON Schema"]
    end
```

这些材料来自**三路并行**的收集（源码：`src/constants/prompts.ts:444` 的 `getSystemPrompt`、`src/context.ts` 的 `getUserContext`/`getSystemContext`）：

- `getSystemPrompt` → 系统提示词（第 1 件）；
- `getUserContext` → CLAUDE.md + 当前日期（第 2 件）；
- `getSystemContext` → git 状态（追加在第 1 件末尾）。

三件材料并行获取（都是启动时预热过的缓存），然后由 `src/utils/api.ts` 的两个函数"各就各位"地摆进请求。逐件拆开看。

### 5.1 系统提示词：Anthropic 写给模型的"员工手册"

每次请求都带的 `system` 字段，是 Anthropic 写给模型的员工手册，分两段：

- **静态段**：角色定义（"你是 Claude Code，一个编程助手"）、行为规范（"修改文件前必须先读文件""回答要简洁"）、工具使用规范等几十条。静态段对所有用户基本一样——**这是故意的**，相同的前缀能命中 API 的 prompt cache（缓存命中部分不计费/少计费），所以源码里 `getAllBaseTools()`（`src/tools.ts:193`）头上挂着一条注释，警告改动必须和缓存配置保持同步。
- **动态段**：当前可用的技能清单、你的记忆摘要、操作系统/shell/模型版本等环境信息。

### 5.2 CLAUDE.md 的收集规则：五个来源，逐级叠加

CLAUDE.md 不是"只读一个文件"，而是一套有严格顺序的收集流程（`src/utils/claudemd.ts:790` 的 `getMemoryFiles`），**依次收集 5 个来源**，全部拼在一起：

```mermaid
flowchart TD
    A["开始收集记忆文件"] --> B["1️⃣ Managed（企业管控）<br/>/etc/claude-code/CLAUDE.md + 管控版 rules<br/><i>最先加载：公司 IT 下发的强制规范</i>"]
    B --> C["2️⃣ User（用户全局）<br/>~/.claude/CLAUDE.md + ~/.claude/rules/*.md<br/><i>你个人对所有项目的偏好</i>"]
    C --> D["3️⃣ Project + Local（项目）<br/>从当前目录<b>逐级向上走到根目录</b><br/>每层找 4 个目标："]
    D --> E["· CLAUDE.md（项目规范，提交进 git）<br/>· .claude/CLAUDE.md（另一种放法）<br/>· .claude/rules/*.md（拆成多条的规则）<br/>· CLAUDE.local.md（私人项目笔记，不提交 git）"]
    E --> F["4️⃣ --add-dir 额外目录<br/><i>需环境变量开启，默认关闭</i>"]
    F --> G["5️⃣ 自动记忆 / 团队记忆<br/><i>实验特性，feature gate 控制</i>"]
    G --> H["全部拼成一大段文字"]
```

几个值得知道的细节：

1. **向上查找意味着"就近叠加"**：你在 `项目A/src/utils` 里启动，根目录、项目A、src、utils 每层的 CLAUDE.md 都会被收集——越靠近你位置的文件越具体，全部生效、互不覆盖，模型自己判断哪条更适用。
2. **每个文件带"身份标签"**（`getClaudeMds`，`claudemd.ts:1153`）：拼进上下文时，每个文件前标注路径和类型，如 `(project instructions, checked into the codebase)`、`(user's private global instructions for all projects)`——模型由此知道哪条是公司强制规范、哪条是你的私人笔记，会区别对待。
3. **@导入语法与安全闸门**（`processMemoryFile`，`claudemd.ts:618`）：CLAUDE.md 里可以写 `@路径/文件.md` 导入别的文件，软件递归展开，但有四道闸门——最多递归 5 层（`MAX_INCLUDE_DEPTH`）；只允许文本扩展名白名单（防止把图片 PDF 塞进记忆）；**项目级文件默认不许导入项目目录之外的文件**（防恶意仓库用 `@~/.ssh/id_rsa` 偷读你的私人文件）；所有路径进 `processedPaths` 集合去重（含软链接归一）。单文件上限 40000 字符（`MAX_MEMORY_CHARACTER_COUNT`）。
4. **可整体关停**：环境变量 `CLAUDE_CODE_DISABLE_CLAUDE_MDS` 或 `--bare` 模式跳过自动发现（`src/context.ts:165`）。

### 5.3 摆放位置规则：指令进 system，记忆伪装成首条用户消息

材料收集齐后，放进请求的位置是有讲究的（`src/utils/api.ts`）。看两段真实源码：

```ts
// api.ts:437 —— git 状态：追加在系统提示词末尾
export function appendSystemContext(systemPrompt, context) {
  return [
    ...systemPrompt,
    Object.entries(context)
      .map(([key, value]) => `${key}: ${value}`)   // 拼成 "gitStatus: <内容>"
      .join('\n'),
  ].filter(Boolean)
}

// api.ts:449 —— CLAUDE.md + 日期：伪装成对话最早的一条 user 消息
export function prependUserContext(messages, context) {
  // ……空上下文直接返回……
  return [
    createUserMessage({
      content: `<system-reminder>\nAs you answer the user's questions,
you can use the following context:\n...（记忆内容+日期）...
IMPORTANT: this context may or may not be relevant...`,
      // isMeta: true —— 模型看得见，界面上不显示
    }),
    ...messages,
  ]
}
```

逐行注释：`appendSystemContext` 把 git 状态格式化成 `key: value` 拼到系统提示词数组末尾——git 状态属于"环境状况"，和指令放一起最合理。`prependUserContext` 则把 CLAUDE.md 包成一条 `<system-reminder>` 用户消息，**插到对话历史的开头**。

为什么不也放进 system 字段？三个设计巧思：

1. **`isMeta: true` 隐身标记**——模型看得见，你的终端界面不显示，对话框不会被污染；
2. **`IMPORTANT: this context may or may not be relevant`**——明确告诉模型"这是背景资料，不是提问"，防止它把 CLAUDE.md 当成需要回答的问题；
3. **缓存友好**：git 状态和 CLAUDE.md 一次会话只读一次（memoize），每轮重复发送相同内容，恰好命中 prompt cache 省钱省时——这也是启动时要"预热"的原因（第三章）。

### 5.4 git 状态：一张"会话开始时"的仓库快照

`getSystemContext` 收集的 git 信息包括：当前分支、主分支名、`user.name`、`git status --short`（**超过 2000 字符截断**，`context.ts:20` 的 `MAX_STATUS_CHARS = 2000`）、最近 5 条 commit。内容里明确注明这是"**会话开始时的快照**"——因为收集是 memoize 的，会话中不会刷新，软件选择如实告知模型"这可能过时"，而不是每轮花几百毫秒重新跑 git 命令。**准确性与速度之间，它选了速度 + 诚实标注。**

> 🔍 **源码指路**：`src/constants/prompts.ts:444`（getSystemPrompt）｜`src/context.ts:20,165`（git 截断、关停开关）｜`src/utils/claudemd.ts:618,790,1153`（@导入、收集顺序、身份标签）｜`src/utils/api.ts:437,449`（appendSystemContext / prependUserContext）

---

## 六、心脏地带：Agent 主循环（最重要的一章）

这一章是整个软件的灵魂。理解这一章，就理解了所有 AI Agent 的工作方式。

### 6.1 循环的骨架：一个 `while (true)`

源码在 `src/query.ts`。入口 `query()`（`:219`）只是个壳，真正的心脏是 `queryLoop`（`:241`），核心结构出奇的简单——一个 `while (true)` 死循环加一个跨轮持有的 `State` 结构体（`:265-284`）：

```ts
async function* queryLoop(params, consumedCommandUuids) {
  // 不可变参数：整个循环期间绝不重新赋值
  const { systemPrompt, userContext, systemContext, canUseTool,
          fallbackModel, querySource, maxTurns } = params

  // 可变状态：跨轮携带。每轮开头解构，继续时整体替换
  let state: State = {
    messages: params.messages,          // ← 对话历史，越滚越长
    toolUseContext: params.toolUseContext,
    maxOutputTokensRecoveryCount: 0,    // 输出截断恢复计数
    hasAttemptedReactiveCompact: false, // 被动压缩只试一次的标记
    turnCount: 1,
    // ……
  }

  while (true) {   // ← query.ts:307，Agent 的全部生命都在这个循环里
    // ① 压缩防线（第十二章）
    // ② deps.callModel 流式请求模型（:659）
    // ③ 收集 tool_use 块（:829-834）
    // ④ 无 tool_use → return completed（:1357）
    // ⑤ 有 tool_use → 执行工具，更新 state.messages，continue
  }
}
```

源码注释里还藏着一段演变史：检查点名叫 `query_recursive_call`（`:1714`），历史注释说明**这个循环以前是递归实现的，后来重构成了 while + State**。为什么改？递归每转一圈就加深一层调用栈，几十轮工具调用后栈越来越深、调试栈追踪难看、还无法在每个"继续点"统一做清理；改成 `while + State` 后，所有跨轮状态集中在一个结构体里，7 个 continue 点统一写 `state = { ... }`，行为一目了然。这是"先写得对，再重构得清晰"的典型轨迹。

### 6.2 循环长什么样：一张总图

```mermaid
flowchart TD
    START(["带着打包好的请求进入循环"]) --> COMPACT["① 压缩防线<br/><i>第十二章细讲</i>"]
    COMPACT --> CALL["② deps.callModel 流式请求<br/><i>src/services/api/claude.ts</i>"]
    CALL --> COLLECT["③ 边收边收集 tool_use 块<br/><i>needsFollowUp 是唯一核心循环信号</i>"]
    COLLECT --> CHECK{"有没有<br/>工具调用？"}
    CHECK -->|"没有"| DONE["🎉 跑 stop hooks 后<br/>return completed（:1357）"]
    CHECK -->|"有"| EXEC["④ 执行工具<br/><i>第七章细讲</i>"]
    EXEC --> APPEND["⑤ state.messages =<br/>[...旧历史, ...assistant回复, ...toolResults]<br/><i>query.ts:1716</i>"]
    APPEND --> COMPACT
```

一句话概括：

> **只要模型还想用工具，循环就不停；模型什么时候不用工具了，答案就出来了。**

### 6.3 完整演示："帮我找出项目中所有 TODO 并汇总"

看一个三圈的真实轨迹。你说："帮我找出项目里所有 TODO 注释并汇总"。

| 圈数 | 发给模型的历史（累积） | 模型的回复 | 软件的动作 |
|---|---|---|---|
| 第 1 圈 | 员工手册+git状态+你的问题 | `tool_use`: Grep(pattern="TODO") | 执行搜索，几十条匹配追加进历史 |
| 第 2 圈 | 第 1 圈全部 + Grep 结果 | `tool_use`: Read(第1个文件) 和 Read(第2个文件)（**并行**） | 两个只读工具同时跑，文件内容追加进历史 |
| 第 3 圈 | 前两圈全部 + 文件内容 | 纯文字："共找到 23 个 TODO，集中在 3 个文件……" | **无 tool_use → 循环结束**，显示答案 |

```mermaid
sequenceDiagram
    participant 循环 as queryLoop
    participant 模型
    participant 工具 as 工具执行器

    循环->>模型: 第1圈：问题+历史
    模型-->>循环: tool_use: Grep("TODO")
    循环->>工具: 执行 Grep
    工具-->>循环: "a.ts:12, b.ts:88, c.ts:5 ..."
    循环->>模型: 第2圈：历史+Grep结果
    模型-->>循环: tool_use: Read(a.ts) + Read(b.ts)
    par 只读工具并行（上限10个）
        循环->>工具: Read a.ts
        循环->>工具: Read b.ts
    end
    工具-->>循环: 两个文件内容
    循环->>模型: 第3圈：更长的历史
    模型-->>循环: 纯文字汇总（无 tool_use）
    循环->>循环: return completed → 显示答案
```

注意历史**越滚越长**：第 3 圈请求包含前两圈全部对话。这就是为什么长对话越来越慢、越来越贵，也是第十二章"压缩"存在的原因。

### 6.4 流式接收：为什么回答是一个字一个字"打"出来的

模型生成回复需要时间，Claude Code 用**流式（streaming）**传输：服务器写一点传一点。发起请求的代码（`claude.ts:1822` 附近）有个耐人寻味的注释：

```ts
// Use raw stream instead of BetaMessageStream to avoid O(n²) partial JSON parsing
// BetaMessageStream calls partialParse() on every input_json_delta, which we don't need
// since we handle tool input accumulation ourselves
const result = await anthropic.beta.messages.create(
  { ...params, stream: true },
  { signal, ... },   // ← signal：ESC 急刹车的"刹车线"直接通到这里
)
```

逐行注释：软件**故意不用 SDK 封装好的高级流对象**，而用裸流。为什么？工具调用的参数是 JSON，流式传输时是碎成一小段一小段（`input_json_delta` 事件）过来的；SDK 每收到一小段就尝试解析一次"还没长全的 JSON"，内容越长重复解析越慢，是 O(n²) 的浪费。Claude Code 自己把碎片**拼成字符串、收齐后只解析一次**，省掉全部重复劳动。当你一次让模型写一个大文件时，这个优化差距非常明显。

流式传输的格式叫 SSE（服务器推送事件），软件收到后进入一个大 `switch`（`claude.ts:1980-2303`）分拣处理：

```mermaid
flowchart LR
    SSE["服务器 SSE 事件流"] --> SW{"事件类型 switch"}
    SW -->|"content_block_start<br/>(:1995)"| A["初始化一个内容块<br/>（文字块或 tool_use 块）"]
    SW -->|"input_json_delta<br/>(:2087)"| B["把工具入参的 JSON 碎片<br/>逐段拼接到字符串上"]
    SW -->|"content_block_stop<br/>(:2171)"| C["块收齐了 → 组装 yield<br/>JSON 只在此刻解析一次"]
    SW -->|"message_delta<br/>(:2213)"| D["回填 usage 统计和<br/>stop_reason（模型为何停笔）"]
```

配套的保命装置：流**空闲看门狗 90 秒**（服务器半天不说话就判定断流，报错重试）；**模型 fallback**——当前模型超载时自动换备用模型重试。

### 6.5 一个精巧的优化：边收边干活

模型的话还没说完，工具已经在跑了。这就是 `StreamingToolExecutor`（`src/services/tools/StreamingToolExecutor.ts`）：**工具调用的参数一旦收齐，不等模型把后面的解释文字说完，立刻开始执行**。比如模型前半段是"我要跑 `npm test`"，后半段解释还在传输中，`npm test` 已经在你的电脑上跑起来了。

执行纪律由工具的 `isConcurrencySafe` 属性决定（`StreamingToolExecutor.ts:105-133`）：

- **只读工具可并行**（读文件、搜索互不干扰），并发上限 10 个（环境变量 `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY` 可调）；
- **写操作严格排队**：同一批里只要有一个不安全的工具，大家就一个一个来，避免互相踩脚；
- **Bash 出错会取消兄弟命令**（`siblingAbortController`，`:48-59`）：一批命令中 `mkdir foo` 失败了，后面"往 foo 里写文件"的命令直接取消——因为命令之间常有依赖链，前面垮了后面没意义。

### 6.6 关键不变式：每封"信"必须有"回信"

这是整个系统里最容易被忽略、却最硬的一条规矩：**API 规定，每个 `tool_use` 块必须配对一条 `tool_result`，否则下一轮请求直接被 API 拒绝（400 错误）**。

但现实中模型可能发出了工具调用请求，却被 ESC 打断、被模型切换打断、被异常打断——这些"孤儿 tool_use"怎么办？答案是 `yieldMissingToolResultBlocks`（`query.ts:123-149`）：在中断/fallback/异常时，软件**自动为每个孤儿补一条假的 tool_result**（内容是"被打断"的错误说明，标 `is_error: true`）。就像写信必须有回信，哪怕回信写的是"对方不想聊了"。没有这层补救，任何一次中断都会让对话永久报废。

### 6.7 循环的紧急出口：终止原因全集

除了 `completed`（模型不再调工具）这个正常出口，循环还有一整套紧急出口（源码里统称 `Terminal` 状态）：

| 终止原因 | 什么时候发生 |
|---|---|
| ✅ `completed` | 模型正常说完，无工具调用 |
| 🛑 `aborted_streaming` / `aborted_tools` | 你在流式接收/工具执行中按了 ESC |
| 📏 `prompt_too_long` | 所有压缩手段都失败，窗口实在装不下 |
| 🔁 `max_turns` | 达到调用方设的轮数上限（如 SDK 的 maxTurns） |
| 💥 `model_error` / `image_error` | API 持续出错 / 图片处理失败 |
| 🪝 `stop_hook_prevented` / `hook_stopped` | 你配置的 Stop 钩子说"停" |
| ⛔ `blocking_limit` | 禁用了压缩时撞到窗口警戒线，留你手动 `/compact` 的余地 |

还有两个"续命"机制值得一提：**输出截断恢复**——模型回复被 `max_output_tokens` 截断时，软件会让它"接着写"，最多恢复 3 次（`MAX_OUTPUT_TOKENS_RECOVERY_LIMIT`）；**token 预算催促**——快没钱（token 预算）时，往对话里注入一条 meta 消息催模型"收尾吧"。

> 🔍 **源码指路**：`src/query.ts:219,241,265-284,307,659,829-834,1357,1714-1716`（循环骨架）｜`src/query.ts:123-149`（补孤儿 tool_result）｜`src/services/api/claude.ts:1822,1980-2303`（裸流与 SSE switch）｜`src/services/tools/StreamingToolExecutor.ts:48-59,105-133`（并发纪律与兄弟取消）

---

## 七、AI 的"手"：工具系统与权限闸门

### 7.1 工具是什么：一个约 40 字段的对象

模型能用的每个能力都是一个"工具（Tool）"对象（定义在 `src/Tool.ts`），约 40 个内置工具。每个工具由五组字段组成：

| 组 | 字段 | 干什么 |
|---|---|---|
| 身份 | `name` / `aliases` | 模型按名字调用 |
| Schema | `inputSchema`（Zod 定义） | 规定参数格式；`zodToJsonSchema` 转成 JSON Schema 发给 API |
| 行为 | `call` / `validateInput` / `isReadOnly` / `isConcurrencySafe` | 真正干活的函数与并发属性 |
| 权限 | `checkPermissions` | 工具自己判断"这个具体操作危不危险" |
| UI | `renderToolUseMessage` 等 | 在终端怎么显示（Bash 显示 `$ 命令`，Edit 显示 diff） |

所有工具经 `buildTool` 工厂（`Tool.ts:783`）统一出炉，工厂会填上 **fail-closed 默认值**——凡是你没明确定义的字段，默认走最保守的行为（宁可多问一次，不放错一次）。全部工具的**唯一事实来源**是 `getAllBaseTools()`（`tools.ts:193`），约 40 个内置工具在此登记；随后 `getTools()` 按当前模式过滤、按 deny 规则剔除，`assembleToolPool()` 再合并 MCP 工具并去重排序——**排序稳定很重要**，因为工具清单每轮都发给 API，顺序稳定才能命中 prompt cache。

工具的"说明书"怎么到模型手里？`toolToAPISchema`（`utils/api.ts:119`）把每个工具转成 `{name, description: await tool.prompt(), input_schema}`，作为每轮请求的 `tools` 参数发出（`claude.ts:1235`）。工具太多时还有 **ToolSearch** 机制（`defer_loading`）：先只给模型一个"工具搜索工具"，它按关键字查到需要的工具后再加载完整 schema——书架先只放书名，用到哪本抽哪本。

### 7.2 权限裁决：一道五道关卡的闸门

这是整个系统安全的核心。模型**没有任何办法**直接执行命令——它只能"提出申请"，批不批在软件（和你）。裁决函数是 `hasPermissionsToUseToolInner`（`src/utils/permissions/permissions.ts:1158`），关卡顺序如下（源码注释原样标注）：

```ts
async function hasPermissionsToUseToolInner(tool, input, context) {
  // 1. Check if the tool is denied        ← 第一关：deny 规则（一票否决）
  const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
  if (denyRule) return { behavior: 'deny', ... }

  // 1b. Check if the entire tool should always ask  ← ask 规则（逢用必问）
  const askRule = getAskRuleForTool(...)
  // ……然后是：工具自己的 checkPermissions（Bash 在此解析命令）
  // ……再是：权限模式（default/plan/acceptEdits/bypassPermissions/dontAsk/auto）
  // ……再是：alwaysAllow 规则（你以前点过"始终允许"的）
  // ……五关都不中 → 兜底 ask：弹窗问你
}
```

```mermaid
flowchart TD
    A["模型申请：Bash('某命令')"] --> B{"① deny 规则？"<br><i>如你把 rm 加进黑名单</i>}
    B -->|命中| DENY["❌ 拒绝，原因回喂模型"]
    B -->|否| C{"② ask 规则？"<br><i>逢用必问的清单</i>}
    C -->|命中| ASK["❓ 弹窗问你"]
    C -->|否| D{"③ 工具自查 checkPermissions<br><i>Bash：解析命令 AST，匹配前缀规则<br>如 Bash(git *)，比对危险模式清单</i>"}
    D -->|判定危险| DENY
    D -->|判定安全| ALLOW["✅ 直接执行"]
    D -->|拿不准| E{"④ 权限模式<br><i>plan 模式全拦截 / acceptEdits 放行改文件<br>bypassPermissions 全放行 / auto 交给分类器</i>"}
    E -->|模式说放行| ALLOW
    E -->|模式说拦| F{"⑤ alwaysAllow 规则？<br><i>你以前点过'始终允许 npm test'？</i>"}
    F -->|命中| ALLOW
    F -->|否| ASK
    ASK -->|允许一次| ALLOW
    ASK -->|始终允许| WRITE["✅ 执行 + 规则写入 settings<br>下次不再问"]
    ASK -->|拒绝| DENY
```

Bash 的"工具自查"值得展开：它会把命令解析成 AST（语法树），做两件匹配——**前缀规则**（你配置过 `Bash(git *)` 允许，那 `git status`、`git log` 都直接放行）；**危险模式清单**（`src/utils/permissions/dangerousPatterns.ts` 里维护着 `python`、`node`、`npx`、`bash` 等"能执行任意代码的解释器"名单，带这些前缀的允许规则会被额外审视——因为 `Bash(python *)` 等于放开了一切）。另外有些场景下软件会让一个小模型先给命令"预判危险等级"（`startSpeculativeClassifierCheck` 投机分类器），高置信度安全的直接放行，减少弹窗打扰。

### 7.3 四种命令，四种命运

把裁决过程落到四个具体例子：

| 命令 | 命运 | 走的关卡 |
|---|---|---|
| `git status` | ✅ **直接放行** | 你之前点过"始终允许 `git *`"→ 命中第 ⑤ 关 alwaysAllow 规则；或工具自查判定只读安全 |
| `rm -rf /` | ❌ **直接拦截** | 命中危险模式/deny 规则，第 ① 或 ③ 关一票否决，根本不弹窗——**有些命令连问都不该问** |
| `npm test` | ❓ **弹窗问你** | 五关都不中 → 兜底 ask。你选"允许一次"就只跑这次；选"始终允许"会把规则写进配置文件，以后 `npm test` 不再烦你 |
| 模型看到"被拒绝"后 | 🔁 **自我纠正** | 拒绝原因会作为工具结果回喂模型，它读到后通常换个姿势再来（比如改用只读方式完成任务） |

注意第四种情形透露的设计哲学：**模型犯错不会搞崩系统**。模型瞎编工具名，软件回一句 `No such tool` 让它自我纠正；参数给错，Zod 校验打回；命令被拒，原因回喂。所有失败都被翻译成模型能读懂的"文字反馈"，循环继续——而不是程序崩溃。

### 7.4 执行链全景：从"模型想用"到"真的执行"

```mermaid
flowchart TD
    A["模型说：tool_use Bash('rm -rf node_modules')"] --> B["1. findToolByName 找工具<br><i>找不到 → 回 'No such tool' 让模型自我纠正</i>"]
    B --> C["2. inputSchema.safeParse 校验参数<br><i>command 是字符串吗？</i>"]
    C --> D["3. validateInput 工具自检"]
    D --> E["4. PreToolUse 钩子<br><i>toolHooks.ts:435，用户脚本可拦截/改写</i>"]
    E --> F{"5. canUseTool 权限裁决<br><i>7.2 节的五道关卡</i>"}
    F -->|拒绝| G["拒绝原因回喂模型"]
    F -->|放行| H["6. tool.call 真正执行（:1207）<br><i>起 shell 进程，收集输出</i>"]
    H --> I["7. 结果格式化 mapToolResultToToolResultBlockParam"]
    I --> J["8. PostToolUse 钩子<br><i>可对结果追加反馈</i>"]
    J --> K["9. 包成 tool_result 的 user 消息<br>追加进对话历史 → 主循环下一圈"]
```

（源码主线：`src/services/tools/toolExecution.ts:337` 的 `runToolUse`。）工具执行抛异常也不会崩：异常被包成 `is_error: true` 的 tool_result 回喂，模型读到错误信息再想办法。

### 7.5 钩子（Hooks）：你自装的"自动化安检门"

钩子是你在配置文件里登记的 shell 命令，在 20 种事件点上被触发（PreToolUse / PostToolUse / UserPromptSubmit / SessionStart / Stop / PreCompact 等）。执行机制（`src/utils/hooks.ts`）是用 `child_process.spawn` 起子进程跑你的脚本：**JSON 数据经 stdin 传入，你的脚本往 stdout 写 JSON 返回**。返回的 JSON 可以表达 `allow`/`deny`/`ask`/`hookUpdatedInput`（改写工具入参）/`additionalContext`（给模型追加资料）/`stop`（停掉循环）。

举例：你可以配一个 PreToolUse 钩子，每当模型要 Edit 文件就自动先跑一遍格式化检查；或配一个 UserPromptSubmit 钩子，把你的每句提问自动翻译成英文再发给模型。**钩子和权限系统的区别在于**：权限是软件内置的闸门，钩子是你自己加装的流水线。

### 7.6 技能与 MCP：工具箱的两种扩充

- **技能（Skill）**：`src/skills/` 扫描 `~/.claude/skills`、项目 `.claude/skills`、插件目录和内置目录，每个 `SKILL.md`（带 YAML frontmatter）是一个技能。采用**渐进披露**：平时只往上下文注入一份技能**清单**（`attachments.ts` 的 `getSkillListingAttachments`，省 token），模型决定用某个技能时才通过 Skill 工具加载完整说明书。
- **MCP 工具**：外部 MCP 服务器提供的工具被 `MCPTool` 包装成同名 Tool 对象，与内置工具一视同仁——同样的 schema 校验、同样的权限裁决、同样的执行链。对模型和权限系统来说，根本分不清一个工具是内置的还是外来的，这正是设计目的。

> 🔍 **源码指路**：`src/Tool.ts:783`（buildTool）｜`src/tools.ts:193`（getAllBaseTools）｜`src/utils/api.ts:119`（toolToAPISchema）｜`src/utils/permissions/permissions.ts:1158`（裁决顺序）｜`src/utils/permissions/dangerousPatterns.ts`（危险模式）｜`src/services/tools/toolExecution.ts:337,1207`（执行链）｜`src/services/tools/toolHooks.ts:435`（PreToolUse）｜`src/utils/hooks.ts`（spawn 执行钩子）

这一章是工具系统的"速览"：讲了工具是什么、权限怎么管。至于约 40 个内置工具各自的长相、参数和怪癖——**逐一上手的完整图鉴见第八章**；技能与 MCP 的完整生态见第十章。

---

## 八、工具图鉴：AI 的 40 件兵器，逐一上手

第七章讲了工具对象的字段结构和权限闸门——那是"制度层面"的速览。这一章把工具箱整个倒在桌上，一件一件看过去：每个工具叫什么名字、干什么活、吃什么参数、有什么怪癖。`src/tools/` 下共 39 个工具目录（另有 testing/ 与 shared/），加上包装 MCP 工具的 MCPTool 家族，合计 40 余件"兵器"，**一件不漏**。

先看三件所有工具共用的基础设施，再按 11 个分组发"图鉴卡片"。

### 8.1 工具池是怎么拼出来的：注册 → 过滤 → 排序

模型手里的工具清单不是一张写死的表，而是每轮请求前现场拼出来的，三道工序：

```mermaid
flowchart LR
    A["① getAllBaseTools() 注册<br/><i>tools.ts:193-251</i><br/><b>唯一事实来源</b>：约 40 个内置工具在此登记<br/>核心约 14 个无条件启用<br/>其余靠 feature()/环境变量门控"]
    A --> B["② getTools() 二次过滤<br/><i>tools.ts:271-327</i><br/>极简模式只留 Bash/Read/Edit<br/>deny 规则整体剔除（模型根本看不到）<br/>REPL 模式隐藏原语工具"]
    B --> C["③ assembleToolPool() 合并排序<br/><i>tools.ts:345-367</i><br/>内置与 MCP 工具合并去重（内置优先）<br/>各自按名排序，<b>内置必须排成连续前缀</b>"]
    C --> D["📮 作为 tools 参数发给 API"]
```

三个工序各有一个值得记住的点：

1. **唯一事实来源**：想知道"这个版本到底有哪些工具"，只看 `getAllBaseTools()` 这一个函数——它头上有注释警告：改动必须和 prompt cache 配置保持同步（第五章讲过，工具清单属于静态前缀，乱动会打翻缓存）。
2. **deny 规则在模型看到清单之前就剔除**（`tools.ts:262-269` 的 `filterToolsByDenyRules`）：你把某个工具加进黑名单，它不是"申请了再被拒"，而是**压根不出现在说明书里**——模型都不知道它的存在，从源头上杜绝。
3. **内置必须排成连续前缀**：MCP 工具追加在内置工具之后，而不是按名字混排。为什么？缓存按前缀匹配，如果把外来的 MCP 工具插进内置工具中间，下游所有缓存键全部打翻，等于每轮白扔钱。

### 8.2 三标记体系：每个工具自带的"安全标签"

每个工具出厂时身上贴着三个布尔标记（默认值在 `Tool.ts:750-761`——**默认全 false**，不声明就按最保守对待）：

| 标记 | 回答的问题 | 谁在读这个标记 |
|---|---|---|
| `isReadOnly` | 这个操作只看不改吗？ | 权限系统（只读更容易放行）、UI 展示 |
| `isConcurrencySafe` | 能和其他工具同时跑吗？ | 流式执行器（6.5 节：只读并行、写串行，上限 10） |
| `isDestructive` | 这个操作有破坏性吗？ | 权限警示（弹窗加警告） |

妙处在于这三个标记**可以是函数**，按入参动态判定。两个典型：

- **Bash 的 `isReadOnly(input)`**（`BashTool.tsx:434-441`）：不是"Bash 永远危险"一刀切，而是解析命令本身——`ls`、`git status` 算只读可并行，`rm` 就不算；
- **ExitWorktree 的 `isDestructive(input)`**（`:168-170`）：`action: "keep"` 保留工作区不算破坏性，`action: "remove"` 删掉才算——同一把刀，切菜和砍人区别对待。

### 8.3 输出太大怎么办：落盘机制

工具结果要塞回对话历史，可一条命令的输出可能几百 KB。每个工具有一个 `maxResultSizeChars` 上限：Bash 30000 字符、Grep 20000（全工具箱最紧的上限之一，强迫模型把搜索条件写精确）、Read 是 Infinity（截断自己做）、多数工具 100000。超限之后怎么办？

```mermaid
flowchart TD
    A["工具执行完毕，结果超大"] --> B{"超过 maxResultSizeChars？"}
    B -->|没超| C["直接作为 tool_result 回传"]
    B -->|超了| D["完整输出写入临时文件落盘"]
    D --> E["tool_result 只带：开头摘要 + 文件路径<br/><i>persistedOutputPath，如 BashTool.tsx:424,292</i>"]
    E --> F["模型需要细节时<br/>用 Read/Grep 去落盘文件里自取"]
```

思路是"不给模型看全貌，但给它留索引"：历史里不塞 5MB 的日志，只塞一句"完整输出在某路径，需要哪段自己读"。这和第十二章压缩防线的 `applyToolResultBudget` 是同一哲学的两处应用。

### 8.4 图鉴卡片怎么读

接下来每张卡片的格式统一：

> **名称** —— 一句话功能
> **关键参数** · **调用例子**（JSON）· **细节**（最有特点的 1-2 个实现/安全设计，带行号）

标记图例：👀 只读 ｜ ⚡ 并发安全 ｜ 🐢 会被 ToolSearch 延迟加载（8.15 节细讲）。

---

### 8.5 文件操作组（4 件）：先读后写的铁律

**`Read`** —— 模型的眼睛：读本地文件。👀⚡
- 参数：`file_path`（绝对路径，必填）、`offset`（起始行）、`limit`（行数）。
- 例子：改 bug 前 `{"file_path": "/repo/src/index.ts", "offset": 100, "limit": 80}`。
- 细节：**双重输出上限**——文件超过 256KB 直接拒读（`utils/file.ts:48` 的 `MAX_OUTPUT_SIZE`），内容超 25000 token 报错（`limits.ts:18`，可用 `CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS` 覆盖）。`limits.ts:9-13` 的注释记录了一次 A/B 实验回滚：超限后"静默截断"输给了"报错"——因为静默截断会让模型以为自己看完了全文，基于残缺信息乱下结论。

**`Edit`** —— 改代码的主力：精确字符串替换。
- 参数（`types.ts:6-19`）：`file_path`、`old_string`、`new_string`、`replace_all`（默认 false）。
- 例子：`{"file_path": "/repo/app.ts", "old_string": "const x = 1", "new_string": "const x = 2"}`。
- 细节：**没 Read 过的文件直接报错** "File has not been read yet"（`FileEditTool.ts:281`）；检测到外部改动（用户手改、linter 改的）要求重新 Read；`.ipynb` 文件拒绝并引导去 NotebookEdit（`:270`）——各工具守各的辖区。

**`Write`** —— 整文件写入/覆盖。
- 参数：`file_path`、`content`。
- 例子：`{"file_path": "/repo/new.ts", "content": "export const …"}`。
- 细节：覆盖已存在的文件前同样强制"先 Read"，并检测"自我上次读取后文件被改过"（`FileWriteTool.ts:203,216`），防止盲覆盖把你刚手改的内容冲掉。

**`NotebookEdit`** —— 编辑 Jupyter notebook 的 cell。🐢
- 参数：`notebook_path`、`cell_id`、`new_source`、`cell_type`（code/markdown）、`edit_mode`（replace/insert/delete）。
- 例子：`{"notebook_path": "/repo/a.ipynb", "cell_id": "c3", "new_source": "print(1)", "edit_mode": "replace"}`。
- 细节：`shouldDefer: true`（`:94`）——平时连说明书都不发给模型，用到才加载（8.15 节）。

为什么"先读后写"是铁律？模型对文件的认识可能过时——你刚在编辑器里改了两行，它不知道。强制 Read 等于写入前先"刷新认知"：

```mermaid
flowchart TD
    A["模型想 Edit/Write 一个文件"] --> B{"本会话 Read 过这个文件吗？"}
    B -->|没读过| C["❌ 报错 File has not been read yet<br/><i>FileEditTool.ts:281</i><br/>模型自我纠正：先 Read"]
    B -->|读过| D{"上次 Read 之后被外部改过吗？<br/><i>你手动改的 / linter 改的</i>"}
    D -->|改过| E["❌ 要求重新 Read<br/><i>FileWriteTool.ts:203,216</i>"]
    D -->|没改| F["✅ 执行写入"]
```

### 8.6 搜索组（2 件）

**`Glob`** —— 按文件名模式找文件，按修改时间排序。👀⚡
- 参数：`pattern`（如 `**/*.ts`）、`path`（搜索根，默认当前目录）。
- 例子：找所有测试文件 → `{"pattern": "**/*.test.ts"}`。
- 细节：ant 内部构建因为内嵌了 bfs/ugrep 等替代品，Glob/Grep 被**整体从工具列表移除**（`tools.ts:198-201`）——工具箱也会按发行版"减肥"。

**`Grep`** —— 基于 ripgrep 的内容正则搜索。👀⚡
- 参数：`pattern`、`path`、`glob`、`output_mode`（content/files_with_matches/count）、`-A/-B/-C`（上下文行）、`-n`、`-i`、`type`、`head_limit`（默认 250）、`offset`、`multiline`。
- 例子：`{"pattern": "fetchUser", "output_mode": "content", "-n": true}`。
- 细节：`maxResultSizeChars: 20_000`（`GrepTool.ts:164`）——前面说过，全工具箱最紧的上限之一。设计意图：搜索结果太泛是模型的常见病，用硬上限倒逼它把正则写准。

### 8.7 命令执行组（3 件）

**`Bash`** —— 模型的双手：执行 shell 命令。
- 参数（`:227-247`）：`command`、`timeout`、`description`（**可选**，`z.string().optional()`——给人看的通俗描述）、`run_in_background`、`dangerouslyDisableSandbox`。
- 例子：`{"command": "npm test", "description": "Run test suite"}`。
- 细节 1：**复合命令拆分匹配**。模型申请 `ls && git push` 时，`preparePermissionMatcher` 把它拆成子命令逐条过权限规则（`BashTool.tsx:447-465`）——你允许过 `Bash(ls *)` 不代表 `git push` 能搭便车，见下图。
- 细节 2：**对模型隐藏内部字段**。`_simulatedSedEdit` 是"sed 预览被批准后"的内部标记，被刻意从模型可见的 schema 中 omit（`:249-259` 注释）——防模型借它绕过权限沙箱任意写文件。输出超 30000 字符走 8.3 节的落盘（`:424,292`）。

```mermaid
flowchart TD
    A["模型申请：Bash('ls && git push')"] --> B["preparePermissionMatcher<br/>把复合命令拆成子命令<br/><i>BashTool.tsx:447-465</i>"]
    B --> C["子命令 1：ls"]
    B --> D["子命令 2：git push"]
    C --> E{"匹配 Bash(ls *)？<br/>✅ 已允许"}
    D --> F{"匹配 Bash(git push *)？<br/>❓ 没有规则"}
    E --> G["任何一条过不了，整条命令都要弹窗<br/><b>权限按最严的子命令算</b>"]
    F --> G
```

**`PowerShell`** —— Windows 上的 PowerShell 版 Bash。
- 参数几乎与 Bash 相同（`:228-233`）。
- 启用条件：`getPlatform()==='windows'`，且 ant 默认开/外部需 `CLAUDE_CODE_USE_POWERSHELL_TOOL`（`utils/shell/shellToolUtils.ts:17-22`；`tools.ts:242`）。目录内自带 `gitSafety.ts`、`destructiveCommandWarning.ts` 等安全检查。

**`REPL`** —— ant 内部的"批量操作 VM"模式开关。
- 启用：`USER_TYPE==='ant'` 且 CLI 入口，`CLAUDE_CODE_REPL=0` 可关（`REPLTool/constants.ts:23-30`）；SDK 不默认开。
- 细节：开启后 Read/Write/Edit/Glob/Grep/Bash/NotebookEdit/Agent 这 8 个原语工具对模型**隐藏**（`REPL_ONLY_TOOLS :37-46`），模型必须通过 REPL 在 VM 里调用——相当于把"直接动手"换成"在沙盘里动手"。

### 8.8 网络组（2 件）

**`WebFetch`** —— 抓取 URL 内容，并用一个小 prompt 让模型对内容加工后返回。👀🐢
- 参数：`url`、`prompt`（对抓取内容做什么）。
- 例子：`{"url": "https://docs.example.com/install", "prompt": "提取安装步骤"}`。
- 细节：`preapproved.ts:14` 维护一份"代码相关域名白名单"，仅允许 WebFetch 的 GET 免审；文件头有 SECURITY WARNING（`:5-11`）：**这份白名单不许其他工具复用**——白名单是按"只读网页"的威胁模型定的，别的工具借去用就超纲了。

**`WebSearch`** —— 联网搜索。🐢
- 参数：`query`（≥2 字符）、`allowed_domains`、`blocked_domains`。
- 例子：`{"query": "vite 7 migration guide"}`。
- 细节：`isEnabled()` 按 API provider 判定——firstParty 直接开，Vertex 要求 Claude 4.0+ 模型。功能可用性取决于"买单方式"（第十一章细讲认证体系）。

### 8.9 子代理与任务管理组（8 件）

**`Agent`** —— 派生子 agent 独立完成任务（隔离上下文，可并行、可后台）。
- 参数（`:82-126`）：`description`（3-5 词）、`prompt`（任务书）、`subagent_type`、`model`（sonnet/opus/haiku）、`run_in_background`、`name`/`team_name`、`mode`、`isolation: worktree`、`cwd`。
- 例子：三次 `{"description": "调研方案A", "prompt": "…", "run_in_background": true}` 并行探索三条路线。
- 细节：`isReadOnly()` 返回 true——Agent 工具**本身不写盘**，权限检查全部下放给子 agent 内部的工具（`:1264-1266`）；输出上限 100000 字符（`:229`）。第十三章会细讲子 agent 的前后两种跑法。

**`TaskOutput`** —— 读取后台任务的输出，可阻塞等待。👀
- 参数：`task_id`、`block`（默认 true）、`timeout`（≤600000ms，默认 30000）。
- 例子：`{"task_id": "a1b2c3", "block": true, "timeout": 30000}`。
- 细节：`isEnabled()` 写死 `"external" !== 'ant'`（`:163` 附近）——ant 内部版反而禁用这个工具，版本差异的又一例。

**`TaskStop`** —— 终止后台任务。🐢
- 参数：`task_id`（兼容旧名 `shell_id`）。
- 例子：`{"task_id": "b7"}`。

**`TaskCreate` / `TaskGet` / `TaskUpdate` / `TaskList`**（TodoV2 四件套）—— 结构化任务清单的增/查/改/列。🐢
- 特点：支持阻塞关系 `addBlocks`/`addBlockedBy`、`owner`、`metadata`；`TaskUpdate` 的 status 接受 `'deleted'`。
- 启用：`isTodoV2Enabled()`（`tools.ts:218-220`）——非交互会话强制开，`CLAUDE_CODE_ENABLE_TASKS` 强开（`utils/tasks.ts:133-139`）。

**`TodoWrite`** —— 经典待办清单写入（content/status/activeForm）。
- 细节：**与 TaskCreate 系互斥**——`isEnabled() { return !isTodoV2Enabled() }`（`:51` 附近）。新旧两套待办系统二选一，不会同时出现在工具箱里：

```mermaid
flowchart LR
    Q{"isTodoV2Enabled()？<br/><i>utils/tasks.ts:133-139</i>"}
    Q -->|"是（新）"| A["✅ TaskCreate/Get/Update/List 上岗<br/>❌ TodoWrite 隐身"]
    Q -->|"否（旧）"| B["✅ TodoWrite 上岗<br/>❌ Task 四件套不注册"]
```

### 8.10 多 Agent 协作组（3 件）

**`SendMessage`** —— 给队友 agent 发消息。
- 参数：`to`（队友名、`*` 广播；UDS_INBOX 下还支持 `uds:<socket>`、`bridge:<session>` 跨进程投递）、`summary`（5-10 词 UI 预览）、`message`。
- 例子：`{"to": "researcher", "summary": "询问认证方案结论", "message": "你查的 OAuth 方案有结论了吗？"}`。
- 细节：`isReadOnly(input)` 动态判定——纯文本消息算只读；`isEnabled()` = `isAgentSwarmsEnabled()`。

**`TeamCreate` / `TeamDelete`** —— 创建/解散 agent 团队。
- TeamCreate 参数：`team_name`、`description`、`agent_type`。
- 启用：`isAgentSwarmsEnabled()`——ant 恒开，外部需 `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` 或 `--agent-teams`（`utils/agentSwarmsEnabled.ts:24-32`；`tools.ts:228-230` 用 lazy require 打破循环依赖）。

### 8.11 用户交互与配置组（4 件）

**`AskUserQuestion`** —— 向你弹 1-4 个选择题（带选项注解），拿结构化回答。👀⚡
- 参数：`questions`（数组，1-4 个，带唯一性校验 refine）。
- 例子：`{"questions": [{"question": "选哪种数据库？", "options": [{"label": "Postgres"}, {"label": "SQLite"}]}]}`。
- 细节：`--channels`（Telegram/Discord 远程通道）激活时 `isEnabled()` 返回 false（`:135-145`）——没人坐在终端前，弹窗会**挂死整个会话**。功能可用性取决于"面前有没有人"。

**`Brief`（API 名 `SendUserMessage`）** —— KAIROS 主动式助手模式下给你发富文本消息，可带附件，`status: proactive` 主动推送。
- 细节：目录名叫 Brief 但 API 名叫 SendUserMessage（`prompt.ts:1`）；`isEnabled()` 需 `feature('KAIROS')` + 运行时 entitlement 双重通过（`:122-135`）。

**`Skill`** —— 按名调用已安装技能（注入其 SKILL.md 指令）。
- 参数：`skill`、`args`。
- 例子：`{"skill": "commit"}`。
- 细节：这是"渐进披露"的第二阶段开关，第十章细讲。

**`Config`** —— 读/改设置项。
- 参数：`setting`、`value`（省略 = 读取）。
- 细节：`isReadOnly(input)` 动态判定——不传 value 的读取算只读（`:90-92`）；仅 `USER_TYPE==='ant'` 注册（`tools.ts:214`）。

### 8.12 模式切换组（4 件）

**`EnterPlanMode`** —— 进入"只规划不动手"模式。无参数（`EnterPlanModeTool.ts:21-26`）。

**`ExitPlanMode`** —— 提交计划求你批准。
- 参数：`allowedPrompts`——声明实施计划所需的权限类别（描述**动作类别**而非具体命令，如"运行测试"）。
- 细节：channels 激活时，进/出两个工具一起禁用（`:167-178`）——避免"进得去出不来"。

**`EnterWorktree`** —— 创建临时 git worktree 隔离工作区。
- 参数：`name`（slug 校验，可选）。

**`ExitWorktree`** —— 退出 worktree。
- 参数：`action: keep | remove`；有未提交改动时必须显式 `discard_changes: true`。
- 细节：`isDestructive` 按入参判定（8.2 节讲过）；**会话级 scope guard** 只认本会话创建的 worktree（`:177-182`）——不能借这个工具动别的会话的工作区。启用：`isWorktreeModeEnabled()` 当前恒 true（`tools.ts:225`）。

### 8.13 调度与触发组（5 件）

**`Sleep`** —— 等待指定时长。
- 细节：不占 shell 进程、可被打断；它的 prompt 建议模型优先用它而不是 `Bash(sleep)`，并提醒"每次唤醒一次 API 调用、prompt cache 5 分钟过期"（`SleepTool/prompt.ts:3`）——连睡觉都要算缓存账。启用：`feature('PROACTIVE') || feature('KAIROS')`（`tools.ts:25-28`）。

**`CronCreate` / `CronDelete` / `CronList`** —— 注册 cron 定时任务，到点把 prompt 入队。🐢
- CronCreate 参数：`cron`（5 字段本地时间）、`prompt`、`recurring`（false = 一次性）、`durable`（true 时持久化到 `.claude/scheduled_tasks.json`，跨重启存活）。
- 例子：`{"cron": "*/30 * * * *", "prompt": "检查 CI 状态", "recurring": true, "durable": true}`。
- 启用：`feature('AGENT_TRIGGERS')`（`tools.ts:29-35`）。

**`RemoteTrigger`** —— 管理远程触发器。
- 参数：`action: list | get | create | update | run` + `trigger_id` + `body`。
- 启用：`feature('AGENT_TRIGGERS_REMOTE')`（`tools.ts:36-38`）。

### 8.14 LSP 与内部/测试工具（3 件）

**`LSP`** —— 调语言服务器做语义代码导航。
- 参数：`operation`（goToDefinition/findReferences/hover/documentSymbol/workspaceSymbol/goToImplementation/prepareCallHierarchy/incomingCalls/outgoingCalls）、`filePath`、`line`、`character`（1-based）。
- 例子：`{"operation": "findReferences", "filePath": "/repo/src/api.ts", "line": 42, "character": 10}`。
- 启用：`ENABLE_LSP_TOOL` 环境变量（`tools.ts:224`）。Grep 是"文本级"搜索，LSP 是"语义级"搜索——知道谁是真正的函数引用，谁只是同名字符串。

**`StructuredOutput`** —— 非交互（SDK/headless）模式下让模型按调用方给的 JSON Schema 输出结构化结果，Ajv 校验（`SyntheticOutputTool/:20`）。
- 启用：仅非交互会话（`:24-28`）——程序调用 Claude 时，要的是能直接 `JSON.parse` 的答案，不是散文。

**`TestingPermissionTool`** —— 测试专用的模拟权限弹窗，仅 `NODE_ENV==='test'` 注册（`tools.ts:244`）——自动化测试里没人点弹窗，用它假扮用户。

另外，`tools.ts` 还引用了一批**本快照中缺失文件**的工具（feature 门控的实验/内部功能）：TungstenTool、SuggestBackgroundPRTool、MonitorTool、SendUserFileTool、PushNotificationTool、SubscribePRTool、VerifyPlanExecutionTool（`CLAUDE_CODE_VERIFY_PLAN=true`）、OverflowTestTool、CtxInspectTool、TerminalCaptureTool、WebBrowserTool、SnipTool、ListPeersTool、WorkflowTool——知道它们存在即可，图鉴按快照实有盘点。

### 8.15 ToolSearch：工具一多，先报菜名后递菜单

40 多个工具，每个的说明书（名字+描述+JSON Schema）都要占 token，**每轮请求全发**是一笔巨款。ToolSearch 的解法（`defer_loading` 机制）：

> **平时只给模型一份"菜名清单"（工具名，无 schema）；模型想吃哪道菜，先调 ToolSearch 查，查到才递上完整"菜单"（完整 schema）。**

```mermaid
sequenceDiagram
    participant 软件
    participant 模型
    软件->>模型: 每轮请求：全部工具名清单（defer_loading:true，无 schema）<br/>+ ToolSearch 工具本体（说明书齐全）
    Note over 模型: 想读文件，但不知道 Read 的参数格式
    模型-->>软件: tool_use: ToolSearch {query: "select:Read,Edit"}
    软件-->>模型: &lt;functions&gt; 块：Read 和 Edit 的完整 JSONSchema
    模型-->>软件: tool_use: Read {file_path: "..."}（现在会用了）
```

几个关键问题：

**谁被延迟？** `isDeferredTool()`（`ToolSearchTool/prompt.ts:62-108`）：**所有 MCP 工具一律延迟**；内置工具里标了 `shouldDefer: true` 的 26 个延迟（NotebookEdit、AskUserQuestion、WebFetch、WebSearch、EnterPlanMode、ExitPlanMode、TodoWrite、TaskCreate/Get/Update/List、TaskOutput、TaskStop、SendMessage、TeamCreate/Delete、CronCreate/Delete/List、ReadMcpResource、ListMcpResources、EnterWorktree、ExitWorktree、Config、LSP、RemoteTrigger）。两类豁免：标了 `alwaysLoad: true` 的（MCP 工具可用 `_meta['anthropic/alwaysLoad']` 声明豁免，`client.ts:1785`）和 ToolSearch 自己——它永远不能迟到，否则没人能查到别的工具。

**怎么开关？** 环境变量 `ENABLE_TOOL_SEARCH`：`'tst'`（默认，总延迟）、`'tst-auto'`（超过阈值才启用，`utils/toolSearch.ts:444-467` 的 `checkAutoThreshold`）、`'standard'`（关闭）。Haiku 不支持 `tool_reference` 会自动降级不用这套（`:204,239-252`）。发请求时被延迟工具以 `defer_loading: true` 标记发给 API（`utils/api.ts:211-223`）。

**两种查法**（`ToolSearchTool.ts:132-216`）：
- **精确选择**：`{"query": "select:Read,Edit"}`——点名要哪几个，直接给 schema；
- **关键词搜索**：`{"query": "slack send"}`——按词打分匹配：MCP 工具按 `mcp__server__action` 拆词、内置工具按 CamelCase 拆词，比如 `slack send` 能命中 `mcp__slack__send_message`。

注册条件：`isToolSearchEnabledOptimistic()` 为真才把 ToolSearch 放进工具列表（`tools.ts:247-249`）。

### 8.16 MCPTool：外来工具的"入籍"包装

`tools/MCPTool/MCPTool.ts` 只是一个占位模板（name: `'mcp'`，方法上标注 "Overridden in mcpClient.ts"）。真正的包装发生在 `services/mcp/client.ts:1743-1832` 的 `fetchToolsForClient`：

1. 向 MCP 服务器发 `tools/list` 拿到工具清单；
2. 每个工具用 `{...MCPTool, name: fullyQualifiedName, ...}` 展开覆盖（`:1769-1773`），克隆出一个"本地版"；
3. **命名规则**：`mcp__<规范化服务器名>__<工具名>`（`mcpStringUtils.ts:48`），如 `mcp__github__create_issue`。SDK server 可设 `CLAUDE_AGENT_SDK_MCP_NO_PREFIX` 跳过前缀（`:1760-1763`）；
4. **安全元数据来自 MCP 注解**：服务器自报的 annotations 被映射成 8.2 节的三标记（`:1795-1809`）；`mcpInfo` 供权限规则做整服务器匹配；
5. 描述超长截断 `MAX_MCP_DESCRIPTION_LENGTH`（`:1789-1794`）；权限默认 passthrough，并建议用户加 localSettings allow 规则。

```mermaid
flowchart LR
    subgraph SR["MCP 服务器自报家门（annotations）"]
        A["readOnlyHint"]
        B["destructiveHint"]
        C["openWorldHint"]
    end
    A --> D["isReadOnly +<br/>isConcurrencySafe"]
    B --> E["isDestructive"]
    C --> F["isOpenWorld"]
    D --> G["包装好的 MCPTool<br/><b>与内置工具一视同仁</b>：<br/>同样 schema 校验、同样五关权限裁决"]
    E --> G
    F --> G
```

配套的三个"小跟班"工具：

- **`McpAuthTool`**（`client.ts:63`）：服务器需要 OAuth 授权时，动态注册一个 `mcp__<server>__authenticate` 工具引导你完成授权（`client.ts:2318,2331`）；
- **`ListMcpResourcesTool`**：列出 MCP 资源（参数 `server?`）；
- **`ReadMcpResourceTool`**：读取 MCP 资源（参数 `server` + `uri`）。

后两个在 `getAllBaseTools` 末尾登记（`tools.ts:245-246`），但被 `getTools()` 当作 specialTools 按需添加（`:301-307`）。

> 🔍 **源码指路**：`src/tools.ts:193-251,262-269,271-327,345-367`（注册/过滤/排序）｜`src/Tool.ts:750-761`（三标记默认值）｜`src/tools/BashTool/BashTool.tsx:227-259,424,434-465`（Bash 参数/隐藏字段/复合命令拆分）｜`src/tools/FileEditTool/FileEditTool.ts:281`（先读后写）｜`src/tools/ToolSearchTool/prompt.ts:62-108`、`ToolSearchTool.ts:132-216`（延迟名单与搜索打分）｜`src/services/mcp/client.ts:1743-1832`（MCPTool 包装）

---

## 九、斜杠命令图鉴：60+ 个快捷指令的工作原理

第四章讲过：敲下 `/` 开头的输入会被分流到命令系统。这一章把命令系统整个摊开：先回顾三种命令类型，再给一份 60+ 个命令的总表，然后挑 8 个最有代表性的拆到源码层，最后教你写自己的命令。

### 9.1 回顾：三种命令，三种命运（外加两个机制）

命令对象有三种类型（`types/command.ts`），命运截然不同：

```mermaid
flowchart TD
    A["你敲下 /xxx 回车"] --> B["parseSlashCommand 解析"]
    B --> C["findCommand 精确匹配<br/><i>commands.ts:688：名字 / 全名 / 别名<br/>查不到 → Unknown command，绝不瞎猜</i>"]
    C --> D{"什么类型？"}
    D -->|"local-jsx 🖥️"| E["渲染终端 UI（设置面板、选择器）<br/><b>不发 AI</b>"]
    D -->|"local ⚙️"| F["执行本地函数（/clear、/cost）<br/><b>不发 AI</b>"]
    D -->|"prompt 📝"| G["展开成一段预制提示词<br/><b>发给 AI</b>"]
```

两个容易忽略的机制：

- **懒加载**：命令的 `index.ts` 只存元数据（名字、描述、类型），函数体是 `load: () => import(...)` 动态加载——比如 `/insights` 的实现有 115KB，你不用它，它就永远不占内存。和第三章 `--version` 快速通道同一个哲学：启动时少做事。
- **注册顺序**：`COMMANDS()` 数组（`commands.ts:258`）对外约 60 个命令，另有 `INTERNAL_ONLY_COMMANDS`（`:225`）仅内部可用。`loadAllCommands()`（`:449`）按 **bundled 技能 → 内置插件技能 → 目录技能 → 工作流 → 插件命令 → 插件技能 → 硬编码内置命令** 的顺序合并，`findCommand` 按数组顺序取第一个匹配者——所以同名的用户/插件技能会**排在**内置硬编码命令之前，你可以"覆盖"内置命令。

### 9.2 总表：60+ 个命令一览

类型图标：⚙️ local（本地函数）｜🖥️ local-jsx（终端 UI）｜📝 prompt（发 AI）。标注 🚪 的是 feature 门控/内部专用。

**① 会话管理**

| 命令 | 类型 | 一句话说明 |
|---|---|---|
| /clear（别名 reset/new） | ⚙️ | 清空对话历史开新会话——实质是一次"软重启"（9.3 节剖析） |
| /compact | ⚙️ | 清空历史但保留摘要；可带参数自定义摘要指令 |
| /resume（别名 continue） | 🖥️ | 恢复之前的对话：弹选择器或按 ID/标题搜索 |
| /branch（别名 fork） | 🖥️ | 在当前节点分叉出对话分支 |
| /rewind（别名 checkpoint） | ⚙️ | 把代码和/或对话回滚到之前的检查点 |
| /rename | 🖥️ | 重命名当前对话；无参自动生成名字（immediate） |
| /export | 🖥️ | 导出对话为 .txt 或复制到剪贴板 |
| /copy | 🖥️ | 复制最后一条回复；/copy N 取倒数第 N 条 |
| /exit（别名 quit） | 🖥️ | 退出 REPL（immediate） |
| /session（别名 remote） | 🖥️ | 显示远程会话 URL 和二维码 |
| /btw | 🖥️ | 不打断主对话、快速问一个支线小问题（immediate） |
| /plan | 🖥️ | 开启计划模式或查看当前计划 |
| /share /summary /teleport /onboarding /backfill-sessions | 🚪 | 内部 stub 五枚，外部构建是空壳 |

**② 模型与配置**

| 命令 | 类型 | 一句话说明 |
|---|---|---|
| /model | 🖥️ | 设置主循环模型；无参弹选择器，有参直接切换 |
| /effort | 🖥️ | 设置模型 effort 档位 low/medium/high/max/auto |
| /fast | 🖥️ | 开关 fast 模式 |
| /advisor | ⚙️ | 设置/取消 advisor 模型 |
| /config（别名 settings） | 🖥️ | 打开设置面板 |
| /output-style | 🖥️ | 已废弃，提示改用 /config |
| /statusline | 📝 | 派 statusline-setup 子代理配置状态栏 |
| /theme | 🖥️ | 更换终端主题 |
| /color | 🖥️ | 设置本会话提示条颜色（immediate） |
| /vim | ⚙️ | Vim/Normal 编辑模式切换 |
| /keybindings | ⚙️ | 打开或创建键位配置文件 |
| /terminal-setup | 🖥️ | 安装 Shift+Enter 换行键绑定 |
| /voice | ⚙️🚪 | 切换语音模式（特性门控） |
| /brief | 🖥️🚪 | 切换极简回复模式（KAIROS，immediate） |

**③ 上下文与记忆**

| 命令 | 类型 | 一句话说明 |
|---|---|---|
| /memory | 🖥️ | 选择并用编辑器打开记忆文件（CLAUDE.md 系列） |
| /init | 📝 | 扫描代码库，生成/改进 CLAUDE.md（新版还可生成技能与 hooks） |
| /context | 🖥️ | 彩色网格可视化当前上下文占用 |
| /add-dir | 🖥️ | 添加新工作目录 |
| /insights | 📝 | 分析所有会话生成 HTML 洞察报告（115KB，懒加载的典型） |
| /files | ⚙️🚪 | 列出上下文中的所有文件（内部） |

**④ 任务与代码**

| 命令 | 类型 | 一句话说明 |
|---|---|---|
| /review | 📝 | 评审 PR（本地走 gh pr diff 让模型写评审） |
| /ultrareview | 🖥️ | 在 Claude Code on the web 跑 10-20 分钟深度找 bug |
| /security-review | 📝 | 对待提交改动做安全评审 |
| /commit | 📝🚪 | 分析改动创建 git commit（本快照仅内部） |
| /commit-push-pr | 📝🚪 | 提交、推送、建 PR 一条龙（内部） |
| /pr-comments | 📝 | 拉取 PR 评论（已迁为插件的过渡壳） |
| /diff | 🖥️ | 查看未提交改动和逐轮 diff |
| /tasks（别名 bashes） | 🖥️ | 列出并管理后台任务 |
| /agents | 🖥️ | 管理子代理配置 |
| /skills | 🖥️ | 列出可用技能 |
| /hooks | 🖥️ | 查看 hook 配置（immediate） |
| /mcp | 🖥️ | 管理 MCP 服务器（immediate） |
| /plugin（别名 plugins/marketplace） | 🖥️ | 管理插件与市场（10+ 子界面，immediate） |
| /reload-plugins | ⚙️ | 应用待生效的插件变更 |
| /ultraplan | 🖥️🚪 | 云端起草 10-30 分钟高级计划（门控） |
| /ide | 🖥️ | 管理 IDE 集成 |
| /sandbox | 🖥️ | 沙箱开关与配置（immediate） |

**⑤ 权限与安全**

| 命令 | 类型 | 一句话说明 |
|---|---|---|
| /permissions（别名 allowed-tools） | 🖥️ | 管理 allow/ask/deny 规则（含"重试被拒操作"） |
| /privacy-settings | 🖥️ | 隐私设置 |
| /doctor | 🖥️ | 诊断安装与设置 |
| /login | 🖥️ | 登录/切换 Anthropic 账户（OAuth2+PKCE） |
| /logout | 🖥️ | 登出 |

**⑥ 远程与协作**

| 命令 | 类型 | 一句话说明 |
|---|---|---|
| /desktop（别名 app） | 🖥️ | 在 Claude Desktop 中继续会话 |
| /mobile（别名 ios/android） | 🖥️ | 显示移动 App 下载二维码 |
| /chrome | 🖥️ | Claude in Chrome 设置 |
| /install-github-app | 🖥️ | 配置 Claude GitHub Actions（完整向导） |
| /install-slack-app | ⚙️ | 安装 Slack 应用 |
| /remote-env | 🖥️ | 配置远程环境 |
| /web-setup | 🖥️ | 设置 Claude Code on the web |
| /bridge | 🖥️🚪 | Remote Control 桥接（门控） |
| /feedback（别名 bug） | 🖥️ | 提交产品反馈 |

**⑦ 信息与帮助**

| 命令 | 类型 | 一句话说明 |
|---|---|---|
| /help | 🖥️ | 帮助与全部命令（渲染运行时命令注册表的"镜子"） |
| /status | 🖥️ | 版本、模型、账户、API 连通性（immediate） |
| /cost | ⚙️ | 会话总成本与耗时（纯本地账本） |
| /usage | 🖥️ | 套餐用量限制 |
| /extra-usage | 🖥️ | 限额用尽后的额外用量配置 |
| /stats | 🖥️ | 使用统计与活跃度 |
| /release-notes | ⚙️ | 版本更新日志 |
| /upgrade | 🖥️ | 升级 Max 套餐 |
| /passes | 🖥️ | 分享免费周给好友 |
| /stickers | ⚙️ | 订购贴纸（彩蛋） |
| /think-back | 🖥️🚪 | 年度回顾（门控） |
| /heapdump | ⚙️🚪 | JS 堆转储到桌面（隐藏） |

补充三条冷知识：`createMovedToPluginCommand.ts` 是"命令迁往插件"的兼容壳工厂（/pr-comments、/security-review 在用）；`REMOTE_SAFE_COMMANDS`（`commands.ts:619`）规定远程模式只留 17 个纯本地 UI 命令；`BRIDGE_SAFE_COMMANDS`（`:651`）规定手机/网页端可远程触发 6 个：/compact、/clear、/cost、/summary、/release-notes、/files。

### 9.3 深度剖析（8 个代表）

#### /clear —— 不只是"清屏"，是一次 8 步软重启

入口 `clear/clear.ts:4` → `conversation.ts:49` 的 `clearConversation()`：

1. 先执行 SessionEnd hooks（1.5s 超时，`:68-74`）；
2. 上报 `tengu_cache_eviction_hint`，提示服务端驱逐提示缓存（`:77-85`）；
3. 后台 agent 任务和主会话任务（Ctrl+B）**存活**，前台任务一律 kill（`:93-107、135-166`）；
4. `setMessages([])` 清空消息，更新 conversationId 强制重绘（`:109-122`）；
5. `clearSessionCaches()` 清会话缓存（保留存活 agent 私有状态）、重置 cwd、清文件读取缓存/已发现技能/嵌套记忆路径（`:124-132`）；
6. 重置 AppState：文件历史快照、attribution、MCP 客户端全部重置（触发重连），保留 pluginReconnectKey（`:168-191`）；
7. `regenerateSessionId({setCurrentAsParent:true})` 发新会话 ID（旧 ID 记为 parent，供血缘分析），存活任务的 TaskOutput 符号链接指向新会话目录（`:203-224`）；
8. 重新持久化 mode/worktree 状态，执行 SessionStart hooks 注入消息（`:230-250`）。

一句话：**不重启进程完成一次"软重启"**，连分析血缘、后台任务存活、hook 生命周期都考虑到了。

#### /compact —— 派 fork agent 写摘要

入口 `compact/compact.ts:40`，流程：

1. 投影掉被 snip 的消息；用户参数作为自定义摘要指令（`:46-52`）；
2. 无自定义指令先试 session memory compaction（更便宜的渐进路径，`:57-83`）；
3. 传统路径：先 microcompact（就地压缩旧工具结果），再 `compactConversation()`；
4. PreCompact hooks 可注入额外摘要指令；返回 prompt too long 则截掉最旧轮次重试，最多 `MAX_PTL_RETRIES` 次（`:460-491`）；
5. **fork agent 缓存共享路径**（`:1151-1203`）：`runForkedAgent` 派只跑 1 轮（`maxTurns: 1`）、无工具的子代理，带与主线程**完全相同的系统提示/工具/消息前缀**命中 prompt cache——源码注释写着动机：旧路径 98% 缓存未命中、每天浪费约 38B token。失败则回退普通流式（`:1280-1326`）；
6. 摘要模板 `BASE_COMPACT_PROMPT`（`services/compact/prompt.ts:61`）：9 个固定小节（Primary Request / Key Technical Concepts / Files and Code Sections / Errors and fixes / Problem Solving / All user messages / Pending Tasks / Current Work / Optional Next Step）；
7. `buildPostCompactMessages` 重建消息序列：压缩边界 + 摘要 + 保留近期消息。

一句话：不是删消息，而是"用缓存友好的 fork 子代理把旧对话写成结构化摘要，以摘要为新地基重建会话"。

#### /init —— 一段超长"采访剧本"

入口 `commands/init.ts:226`（prompt 型）。命令本身**一行扫描都不做**——它只是返回一段提示词，让主模型自己干活：

- 旧版（`:6-26`）：让模型分析代码库写 CLAUDE.md，明确"该写"（构建/lint/测试命令、高层架构）和"不该写"（显而易见的建议、逐文件罗列）；
- 新版（`:28-224`）：8 阶段剧本——P1 AskUserQuestion 问建项目/个人文件 → P2 派子代理调研代码库 → P3 采访用户补盲 → P4/5 写文件（每行过"删掉会不会让 Claude 犯错"测试）→ P6 创建技能 → P7 创建 hooks → P8 总结。

一句话：**prompt 型命令的极致形态——"命令即剧本"**。这也回答了"为什么 /init 要耗 token"：它本质是发给 AI 的一篇长 prompt。

#### /login —— PKCE + 回环监听双通道

入口 `login/login.tsx:19` → `ConsoleOAuthFlow`（`components/ConsoleOAuthFlow.tsx`）：

```mermaid
sequenceDiagram
    participant CLI as Claude Code CLI
    participant 浏览器
    participant AS as Anthropic 授权服务器

    CLI->>CLI: ① 生成 PKCE code_verifier/code_challenge + state<br/><i>services/oauth/client.ts:47-87</i>
    CLI->>CLI: ② 起临时 localhost HTTP 服务器监听 /callback<br/><i>auth-code-listener.ts:10-16</i>
    CLI->>浏览器: ③ 打开授权 URL（带 code_challenge）
    浏览器->>AS: 用户登录并授权
    AS-->>浏览器: 302 跳回 localhost:PORT/callback?code=...
    浏览器-->>CLI: ④ 回环通道自动捕获 code，校验 state
    Note over CLI: 3 秒没跳转？显示手动粘贴框（:157-181）<br/>——SSH 无浏览器环境也能完成
    CLI->>AS: ⑤ exchangeCodeForTokens 换 token（:103-109）
    AS-->>CLI: access_token + refresh_token
    CLI->>CLI: ⑥ installOAuthTokens 存安全存储（cli/handlers/auth.ts:44）<br/>⑦ 收尾：重建客户端、剥旧 thinking 签名、<br/>resetCostState、authVersion+1（login.tsx:25-55）
```

一句话：标准 OAuth2+PKCE，**双通道**（回环自动 / 手动粘贴）保证 SSH 无浏览器环境也能完成登录。第十一章还会讲 token 存哪、过期怎么续。

#### /resume —— 会话考古学家，跨项目宁给命令不越界

入口 `resume/resume.tsx`：无参时 `getWorktreePaths` + `loadSameRepoMessageLogs` + `filterResumableSessions` 渲染 LogSelector（支持语义搜索）；选中后 `checkCrossProjectResume` 判断——**同仓库 worktree 直接恢复；不同项目不恢复**，而是拼一条 `claude --resume ...` 命令复制到剪贴板（`:145-164`），让你自己到新目录里跑。带参先 UUID 精确匹配再按标题搜；读的是 `~/.claude/projects/<项目>/<sessionId>.jsonl` 转录文件。

为什么跨项目"宁给命令不越界"？会话绑定工作目录和项目上下文，原地切项目等于让一个会话同时效忠两个仓库——安全边界和记忆体系都会错乱。给你命令让你去对面重新开局，是最克制也最安全的做法。

#### /permissions —— 五标签页 + 重试被拒操作

入口 `permissions/permissions.tsx`：`PermissionRuleList` 分**五个标签页**（recent/allow/ask/deny/workspace）；规则来自分层 settings 并标注来源（用户级/项目级/本地）；`applyPermissionUpdate` 内存生效 + `persistPermissionUpdate` 写文件；还会检测"被遮蔽规则"（你加了条永远轮不到的规则，它提醒你）。最有意思的是 `onRetryDenials`：把最近被拒的命令包成 `createPermissionRetryMessage` 塞回队列**让模型重试**——你在权限页放行了某操作后，不必重新描述任务，模型自己会捡起来再来一遍。

#### /cost —— 纯本地账本

入口 `cost/cost.ts:6`：订阅用户显示订阅提示；API 用户 `formatTotalCost()`（`cost-tracker.ts:228`）输出总成本/时长/代码增删行/按模型分组的 token 用量（含 cache 读写）。**数据全部来自进程内累计器，不发任何网络请求**——这是 local 型命令的典型：纯函数格式化内存计数。所以 /cost 永远秒回，且不耗 token。

#### /commit —— prompt 命令教科书（本快照仅内部）

入口 `commands/commit.ts:76`，它把 prompt 型命令的三板斧用到了极致：

```mermaid
flowchart TD
    A["你敲 /commit"] --> B["提示词里嵌四处 !`...` 预执行：<br/>git status · git diff HEAD ·<br/>git branch --show-current · git log --oneline -10<br/><i>commit.ts:16-19</i>"]
    B --> C["本地先执行这 4 条命令<br/>输出替换进提示词文本"]
    C --> D["展开后的完整提示词发给模型：<br/>「这是仓库现状，请按安全协议提交」"]
    D --> E["安全协议约束模型：<br/>不改 git config · 不跳 hooks · 不 amend<br/>不提交疑似密钥"]
    D --> F["allowedTools 白名单：<br/>只放行 git add/status/commit"]
    D --> G["两段式任务：先起草聚焦 why 的 message<br/>再 HEREDOC 单条消息 stage+commit+署名"]
```

一句话：**预执行拿现状、白名单管手脚、安全协议管底线**——自定义命令能学到的招式全在这一页。

### 9.4 自定义命令教程：写一个自己的 /review-fix

内置命令再强，也不如自己写的顺手。自定义命令有两种格式、四个加载位置。

**加载位置与格式**（`skills/loadSkillsDir.ts:638-803`）：

| 位置 | 格式 | 说明 |
|---|---|---|
| `~/.claude/commands/*.md` | 旧格式（`:566` 标注 commands_DEPRECATED） | 单文件即命令 |
| `~/.claude/skills/<名>/SKILL.md` | 新格式 | 目录 + SKILL.md，可带附件文件 |
| `<项目>/.claude/commands/*.md` | 旧格式 | 项目级，随 git 提交共享给团队 |
| `<项目>/.claude/skills/<名>/SKILL.md` | 新格式 | 只认目录格式，单个 .md 不会被当作技能（`:424-428`） |

（另有企业 managed 策略目录与 `--add-dir` 额外目录；插件命令走 `loadPluginCommands.ts:414`。）

**命名空间**：子目录变成冒号前缀——`.claude/commands/frontend/lint.md` → `/frontend:lint`（`buildNamespace :523-552`）；插件命令带 `plugin:command` 前缀。同名按 realpath 先到先得（`:725-763`）。

**模板能力**（`getPromptForCommand :344-399`）：

1. **参数替换**（`utils/argumentSubstitution.ts`）：`$ARGUMENTS` 全部参数；`$ARGUMENTS[0]`/`$0` 第 N 个；frontmatter 写 `arguments: ticketId priority` 后可用 `$ticketId` 命名参数；没写任何占位符时自动在末尾追加 `\n\nARGUMENTS: <args>`（`:140-142`）。
2. **shell 预执行**（`utils/promptShellExecution.ts:69`）：行内 `` !`cmd` `` 与块级 ` ```! ` 两种语法，发给模型**之前**由 BashTool 执行，stdout/stderr 替换进文本；执行前过 `hasPermissionsToUseTool`；技能的 allowed-tools 临时并入 always-allow。**安全红线：MCP 加载的技能一律跳过 shell 预执行**（`:371-374`）——远程内容不可信，不能让一个网络服务器在你电脑上跑命令。
3. **变量注入**：`${CLAUDE_SKILL_DIR}`（技能自己的目录）、`${CLAUDE_SESSION_ID}`。
4. **执行方式**：默认 inline 展开进当前对话；frontmatter 写 `context: fork` 时作为子代理运行（`executeForkedSlashCommand`，`processSlashCommand.tsx:62`），可配 `agent:` 指定子代理类型。

**frontmatter 字段速查**（`parseSkillFrontmatterFields :185-265`）：

| 字段 | 作用 |
|---|---|
| description | 一句话描述（缺省从正文提取） |
| argument-hint / arguments | 参数提示 / 命名参数 |
| allowed-tools | 技能运行期间可用的工具白名单 |
| when_to_use | 告诉模型什么场景该调用 |
| model / effort | 指定模型（可 inherit）/ 思考力度 |
| disable-model-invocation | 只许人调用，模型不能调 |
| user-invocable: false | 用户在 / 菜单里看不到 |
| context: fork / agent | 在 fork 出的子上下文执行 / 指定子代理 |
| paths | 条件技能：匹配路径的文件被读写时才激活 |
| shell | 内嵌 shell 命令用哪个解释器（如 powershell） |
| hooks / version / name | 技能级钩子 / 版本 / 显示名 |

**完整实例：写一个 /review-fix 命令**。需求：让 Claude 按团队规范审查当前 git 改动并直接修复问题。新建 `.claude/commands/review-fix.md`：

```markdown
---
description: 审查当前改动并直接修复发现的问题
allowed-tools: Bash(git *), Read, Edit, Grep
argument-hint: [严重级别，如 critical]

# 任务

当前未提交的改动如下：

!`git diff HEAD`

请完成：
1. 按团队规范（CLAUDE.md）审查以上 diff，只报告 $ARGUMENTS 级别以上的问题；
2. 对每个问题直接用 Edit 修复；
3. 修复后运行 !`git status --short` 确认改动范围没有越界。

安全要求：不要提交，不要推送。
```

敲 `/review-fix critical` 后，完整生命周期是（`processSlashCommand.tsx:827` 的 `getMessagesForPromptSlashCommand`）：注册技能 hooks → `addInvokedSkill` 记录 → 生成命令元数据消息 + isMeta 展开正文（此时执行两处 `` !`...` `` 预执行）→ 解析 @文件附件 → 附 `command_permissions` 附件 → `shouldQuery: true` 发给模型。

### 9.5 immediate 命令：为什么有的命令不用排队

模型正忙着写代码时，你敲的普通消息会进队列（第四章）。但 `/theme`、`/status` 这类**纯本地 UI 命令**排队等模型忙完再执行，体验极差——你只是想换个主题而已。于是有了 `immediate: true`：命中时直接 `load().call()` 执行，**绕过排队管线**（`REPL.tsx:3158-3184`），并上报 `tengu_immediate_command_executed`。

- **哪些命令有**：/btw、/color、/exit、/hooks、/mcp、/plugin、/rename、/sandbox、/status、/brief；/model、/effort、/fast 条件启用（`shouldInferenceConfigCommandBeImmediate`，`utils/immediateCommand.ts:11`）。
- **键位触发一律 immediate**：不管你按快捷键唤起的是什么命令，都按 immediate 处理——键位操作本身就是"我现在就要"的明确信号。
- **bridge 安全白名单**：从手机/网页端发来的命令另有 `isBridgeSafeCommand`（`commands.ts:672`）把关：local-jsx 一律拒绝（远程没终端可渲染选择器）、prompt 放行、local 需白名单。这条防线源自 PR #19134 的教训：手机端曾能触发本地 Ink 选择器弹窗——弹窗出现在你看不到的终端里，会话就此卡死。

> 🔍 **源码指路**：`src/commands.ts:225,258,449,688,619,651`（注册/查找/安全名单）｜`src/utils/processUserInput/processSlashCommand.tsx:525,62,827`（分发/fork/展开）｜`src/commands/conversation.ts:49`（/clear 软重启）｜`src/commands/commit.ts:16-19,76`（预执行+白名单）｜`src/skills/loadSkillsDir.ts:185-265,344-399,523-552,638-803`（frontmatter/模板/命名空间/加载位置）｜`src/utils/promptShellExecution.ts:69,371-374`（预执行与 MCP 红线）｜`src/screens/REPL.tsx:3158-3184`（immediate）

---

## 十、生态组件：插件、技能、MCP 各是什么

内置工具和命令再丰富，也装不下全世界的需求。Claude Code 留了三扇对外开放的门：**插件**（打包分发一整套能力）、**技能**（按需取用的说明书）、**MCP**（接入外部服务的标准接口）。三者常被打包在一起出现，但解决的问题完全不同——10.4 节有对比表。

### 10.1 插件：一次安装全部到位的"能力包"

**比喻**：插件就像 VS Code 的扩展。单个 `.claude/commands/xxx.md` 文件太零散，没法方便地分享和版本管理一整套工作流；插件把斜杠命令、技能、subagent 定义、hooks、MCP 服务器配置、输出样式**打包成一个目录**，一次安装全部到位。

**目录结构约定**（`utils/plugins/pluginLoader.ts:16-28` 文件头注释；`createPluginFromPath()` 在 `:1357`）：

```mermaid
flowchart TD
    subgraph 插件目录["📦 一个插件 = 一个目录"]
        M["plugin.json<br/>清单文件（名字/版本/描述）"]
        C["commands/<br/>斜杠命令 *.md"]
        A["agents/<br/>subagent 定义"]
        S["skills/<br/>技能 SKILL.md"]
        H["hooks/hooks.json<br/>钩子脚本配置"]
        O["output-styles/<br/>输出样式 *.md"]
    end
    M --> L["createPluginFromPath 加载"]
    C --> L
    A --> L
    S --> L
    H --> L
    O --> L
    L --> P["commands/agents/skills/output-styles<br/>四个可选目录<b>并行探测</b>是否存在<br/><i>pluginLoader.ts:1392-1398</i>"]
```

**市场（Marketplace）= git 仓库 + marketplace.json**。一个 marketplace 本质是一个 git 仓库（或 URL/NPM 包），内含 `marketplace.json` 目录清单，登记它提供哪些插件。官方市场是 GitHub 仓库 `anthropics/claude-plugins-official`（`utils/plugins/officialMarketplace.ts:13-27`）。完整流程：

1. `/plugin marketplace add anthropics/claude-plugins-official` → `addMarketplaceSource()`（`marketplaceManager.ts:1782`）；
2. `gitClone` 用**部分克隆（partial clone）+ 稀疏检出**把仓库克隆到 `~/.claude/plugins/marketplaces/`（`:803`、`1034 reconcileSparseCheckout`）——一个市场仓库可能有几十个插件，稀疏克隆只拉你装的那几个，**省流量省磁盘**；
3. `/plugin install feature-dev@claude-plugins-official` → `installPluginFromMarketplace()`（`pluginInstallationHelpers.ts:506`）落地；
4. 下次启动 `loadAllPlugins()`（`pluginLoader.ts:3096`）统一加载，插件命令就出现在 `/` 列表里，来源标注为 plugin。

**加载优先级**：第九章讲过的命令合并顺序——bundled 技能 → 内置插件技能 → 目录技能 → 工作流 → 插件命令 → 插件技能 → 硬编码内置命令（`commands.ts:447-470`）。`findCommand` 取第一个匹配者，所以**插件命令可以覆盖内置硬编码命令**。

**内置插件**：随 CLI 一起发布、可在 `/plugin` 界面开关的插件（`plugins/builtinPlugins.ts:21-32`），ID 形如 `{name}@builtin`，没有文件路径（`path: 'builtin'` 哨兵值，`:85`），开关状态持久化到 settings 的 `enabledPlugins`（`:71-76`）。界面在 `commands/plugin/`（`ManagePlugins.tsx`、`BrowseMarketplace.tsx`、`AddMarketplace.tsx` 等 10+ 个子界面）。

### 10.2 技能：按需取用的说明书

**比喻**：工具的说明书全塞进抽屉（系统提示）会太重。所以 Claude Code 只在抽屉上贴一页**目录**（技能名 + 一句话描述）；模型决定要用某本说明书时，才通过 Skill 工具把整本拿出来读。这就是**渐进披露（Progressive Disclosure）**。

```mermaid
sequenceDiagram
    participant 软件
    participant 模型
    Note over 软件: 第一阶段：只注入清单（省 token）
    软件->>软件: getSkillListingAttachments<br/><i>attachments.ts:2661</i><br/>所有技能的 name/description/whenToUse<br/>打包成一个 skill_listing 附件，按 token 预算裁剪
    软件->>模型: 请求里夹带技能清单（每个 agent 只发一次，增量补发 :2698-2732）
    Note over 模型: 用户说"把 report.pdf 第 3 页的表抠出来"<br/>→ 清单里有 pdf 技能，决定使用
    模型-->>软件: tool_use: Skill {skill: "pdf", args: "..."}
    Note over 软件: 第二阶段：此刻才加载全文
    软件->>软件: getPromptForCommand（loadSkillsDir.ts:344-399）：<br/>拼入 Base directory + SKILL.md 完整正文<br/>替换 $ARGUMENTS/${CLAUDE_SKILL_DIR}/${CLAUDE_SESSION_ID}<br/>执行内嵌 !`shell`（MCP 技能除外）
    软件-->>模型: 完整技能说明书（含 scripts/extract.py 的存在）
    模型-->>软件: tool_use: Bash 跑提取脚本
```

**关键细节**：

- **格式**：只认 `技能名/SKILL.md` 目录格式，单个 `.md` 文件不会被当作技能（`loadSkillsDir.ts:424-428`）。
- **四层加载位置**（`:638-714`）：企业托管（managed）→ 用户级 `~/.claude/skills` → 项目级 `.claude/skills`（含 `--add-dir`）→ 旧版 `/commands/` 目录。
- **frontmatter 字段**：name、description、when_to_use、allowed-tools、argument-hint/arguments、version、model、effort、disable-model-invocation、user-invocable、hooks、context: fork、agent、shell、paths（解析入口 `parseSkillFrontmatterFields :185-265`，详见 9.4 节字段表）。
- **条件技能（paths）**：`parseSkillPaths :159-178` 支持 gitignore 风格路径过滤——只有当匹配路径的文件被读写时才激活（`activateConditionalSkillsForPaths :997` 起）。比如一个 `paths: ["**/*.py"]` 的技能，只在出现 Python 文件时出现在清单里。
- **动态发现**：会话中读写文件时会从文件路径向上递归发现嵌套的 `.claude/skills`（`discoverSkillDirsForPaths :861-915`），跳过 gitignore 目录，深层目录的技能覆盖浅层同名技能（`:944-951`）。
- **内置 bundled 技能**：编译进二进制（`skills/bundledSkills.ts:53`），目录 `skills/bundled/`：`batch`、`claudeApi`、`claudeInChrome`、`debug`、`keybindings`、`loop`、`loremIpsum`、`remember`、`scheduleRemoteAgents`、`simplify`、`skillify`、`stuck`、`updateConfig`、`verify`。带附件的技能首次调用时才把参考文件解包到磁盘，并做了 `O_NOFOLLOW|O_EXCL` 防符号链接攻击（`:176-193`）。
- **与旧版 commands 的关系**：旧格式 `.claude/commands/*.md` 已标注 DEPRECATED，被同一套加载逻辑兼容；**安全红线**再强调一次——MCP 来源的技能永不执行内嵌 shell 预执行（`:372-374`），远程内容不可信。

**实例：写一个自己的技能**。新建 `~/.claude/skills/weekly-report/SKILL.md`：

```markdown
---
name: weekly-report
description: 汇总本周 git 提交生成中文周报
when_to_use: 用户要求写周报、周总结时
allowed-tools: Bash(git *), Read, Write
---

# 周报生成流程

1. 运行 `git log --since="7 days ago" --pretty=format:"%s%n%b"` 收集提交；
2. 按功能模块分组，每组提炼一句"本周进展"；
3. 参照 ${CLAUDE_SKILL_DIR}/template.md 的格式，写入 weekly-report.md。
```

从下周起，你说"帮我写周报"，模型就会自己找到这本说明书照做。

### 10.3 MCP：AI 界的 USB-C 接口

**比喻**：MCP（Model Context Protocol）是"AI 界的 USB-C"——任何外部服务（数据库、Figma、GitHub、公司内部系统）只要按协议实现一个服务器，Claude Code 就能即插即用它的工具、资源和提示词模板。客户端实现是 `services/mcp/client.ts`（3348 行），基于官方 `@modelcontextprotocol/sdk`。

**6 种传输方式**（schema 在 `services/mcp/types.ts:28-131`）：

| 传输 | 适用场景 | 实现位置 |
|---|---|---|
| `stdio` | 本地子进程（最常见：npx 起个进程） | `client.ts:950` |
| `sse` | HTTP 长连接推送 | `client.ts:673,702` |
| `http` | Streamable HTTP | `client.ts:861,900` |
| `ws` / `ws-ide` | WebSocket / IDE 内嵌 | `client.ts:734,783` |
| `sdk` | SDK 进程内传输 | `client.ts:3278` |
| 其他 | `sse-ide`、`claudeai-proxy` 等特殊类型 | — |

**配置与启动审批**：项目根目录的 `.mcp.json` 登记服务器（企业级还有 `managed-mcp.json`，`services/mcp/config.ts:64-66`）：

```json
{
  "mcpServers": {
    "github": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] }
  }
}
```

**为什么不可信仓库要审批？** 和第三章的信任对话框同一逻辑：`.mcp.json` 是项目级配置，可能来自你刚 `git clone` 的陌生仓库——如果 clone 下来就自动起服务器，等于恶意仓库能在你电脑上自动跑进程。所以启动时对每个 pending 服务器弹审批对话框（`handleMcpjsonServerApprovals()`，`services/mcpServerApproval.tsx:13-40`），你的选择记在 settings 的 `enabledMcpjsonServers`/`disabledMcpjsonServers`（`utils.ts:351` 起），加载时只放行 approved 的（`config.ts:1164-1168`）。`--dangerously-skip-permissions` 和非交互模式下自动批准，但那段代码带着严格的防 RCE 注释（`utils.ts:383-400`）。

**三类能力**：① **工具**——包装成 `mcp__服务器名__工具名` 的 MCPTool（8.16 节）；② **资源**——用 ListMcpResourcesTool/ReadMcpResourceTool 浏览读取；③ **prompts**——被转成 Command（`client.ts:2038-2077`，名字同样是 `mcp__server__prompt` 格式）。权限规则支持**服务器级通配**：一条 `mcp__github` 或 `mcp__github__*` 规则管整个服务器（`utils/permissions/permissions.ts:236-259`）。

**连接状态机**（`types.ts:183-248`）：管理 UI 由 `/mcp` 命令提供，React 侧是 `MCPConnectionManager` + `useManageMCPConnections` 负责重连开关：

```mermaid
stateDiagram-v2
    [*] --> pending: 发现 .mcp.json 里的新服务器
    pending --> connected: 你批准 → 连接成功
    pending --> disabled: 你拒绝/禁用
    connected --> failed: 进程崩溃/网络断开
    failed --> connected: 自动重连成功
    connected --> needs_auth: 服务器要求 OAuth
    needs_auth --> connected: mcp__server__authenticate 引导授权完成
    connected --> disabled: /mcp 里手动禁用
    disabled --> connected: /mcp 里手动启用
```

**MCP OAuth**：服务器需要登录授权时进入 `needs-auth`，走标准 OAuth 流程（动态注册的 McpAuthTool 引导），授权成功后回到 connected。

### 10.4 插件 vs 技能 vs MCP：一张对比表

| 维度 | 插件 Plugin | 技能 Skill | MCP |
|---|---|---|---|
| **解决什么问题** | 一整套工作流的打包分发与版本管理 | 教会模型"做某类事的 SOP"，按需加载省 token | 接入外部服务的实时数据与能力 |
| **形态** | 一个目录（可含命令/技能/agents/hooks/MCP 配置） | 一个 SKILL.md（+可选附件） | 一个外部服务器进程/服务 |
| **谁能提供工具** | 间接能（通过内嵌 MCP 配置） | 不能——它只是提示词 | **能**——mcp__server__tool |
| **信任边界** | 装前你看过市场/仓库 | 本地文件可信；**MCP 来的技能不可信**（禁 shell 预执行） | 项目 .mcp.json 需启动审批 |
| **典型来源** | git 市场（如官方 anthropics/claude-plugins-official） | 自己写 / 插件附带 / 内置 bundled | 第三方厂商或自建 |
| **管理入口** | /plugin | /skills | /mcp |

一句话区分：**技能是"说明书"，MCP 是"新器官"，插件是"把说明书和新器官打包卖的礼盒"。**

> 🔍 **源码指路**：`src/utils/plugins/pluginLoader.ts:1357,1392-1398,3096`（插件加载）｜`src/utils/plugins/marketplaceManager.ts:803,1034,1782`（稀疏克隆/加市场）｜`src/skills/loadSkillsDir.ts:185-265,424-428,638-714,997`（技能解析/加载/条件激活）｜`src/utils/attachments.ts:2661`（技能清单附件）｜`src/services/mcp/client.ts:1743-1832,2038-2077`（工具/prompt 包装）｜`src/services/mcpServerApproval.tsx:13-40`（启动审批）｜`src/services/mcp/types.ts:28-131,183-248`（传输与状态机）

---

## 十一、功能特性拆解：effort、登录、模型切换……

前面十章讲了主干机制。这一章把散落在各处的"功能开关"集中拆解：每项按同一套路——**是什么（比喻）→ 源码怎么实现（行号）→ 一个例子**。

### 11.1 effort / 思考预算：给模型的"打草稿时间"调档

**是什么**：模型回答前可以先"打草稿"（thinking），草稿长度可以调。`effort` 是新世代的统一旋钮（low/medium/high/max 四档），`thinking budget` 是老式的精确 token 数；Claude 4.6 系还支持 **adaptive**——模型自己决定想多久。比喻：同一个人，你可以让他"随口答"（low）也可以让他"想清楚再说"（max）。

**源码怎么实现**：

```mermaid
flowchart TD
    A["resolveAppliedEffort 优先级链<br/><i>utils/effort.ts:152-168</i>"] --> B{"① 环境变量<br/>CLAUDE_CODE_EFFORT_LEVEL？"}
    B -->|有| F["✅ 用它"]
    B -->|无| C{"② 会话状态<br/>（/effort 或 /model 设的<br/>appState.effortValue）？"}
    C -->|有| F
    C -->|无| D["③ 模型默认值"]
    D --> F
    F --> G{"clamp：模型支持吗？<br/><i>modelSupportsEffort :26-50</i>"}
    G -->|"max 仅 Opus 4.6 支持<br/>（:55-69）"| H["不支持 max → 自动降到 high"]
    G -->|支持| I["发给 API：<br/>outputConfig.effort（claude.ts:453-455）<br/>数字档走内部 effort_override（:458-463）"]
    H --> I
```

- 档位定义在 `utils/effort.ts:14-20`（另允许数字档，内部用）；`/effort` 命令在 `commands/effort/effort.tsx:16-20`；持久化时数字档和外部用户的 `max` 不写入 settings（`toPersistableEffort :101-112`）。
- **thinking 的两种形态**（`utils/thinking.ts:10-13`）：4.6 系发 `{type: 'adaptive'}` 不带预算；老模型发 `{type: 'enabled', budget_tokens: N}`，预算 = `min(maxOutputTokens-1, 配置值 ?? 模型默认)`（`claude.ts:1617-1627`）。默认开启，`MAX_THINKING_TOKENS=0` 或 settings `alwaysThinkingEnabled: false` 可关（`:146-162`）。
- **彩蛋**：输入里写 `ultrathink` 会触发高亮和更高思考档位（`thinking.ts:29-31` 的关键词检测 + 彩虹色渲染 `:60-86`），由 feature flag `ULTRATHINK` + GrowthBook `tengu_turtle_carbon` 双重门控。

**例子**：你 `/effort high`，下条请求的 API body 里就是 `output_config: {effort: 'high'}`；若模型是 Opus 4.6，thinking 字段为 `{type: 'adaptive'}`，状态栏同步显示档位。

### 11.2 API 登录与认证：四种买单方式与带锁的 token 刷新

**是什么**：同一张电话卡可以走合约套餐、充值卡或公司专线——Claude Code 支持四类"买单方式"：Claude 订阅账号（OAuth）、API key 按量付费、企业云渠道（AWS Bedrock / Google Vertex / Azure Foundry）。

**源码怎么实现**：

- **Provider 判定**（`utils/model/providers.ts:6-13`）：看环境变量 `CLAUDE_CODE_USE_BEDROCK`/`CLAUDE_CODE_USE_VERTEX`/`CLAUDE_CODE_USE_FOUNDRY`，都不设则是 `firstParty`。
- **OAuth 流程**：9.3 节的 /login 剖析已讲（PKCE + 回环监听双通道，`services/oauth/index.ts:14-120`）。
- **凭据存储**：macOS 上 OAuth token 存**钥匙串**（`utils/secureStorage/index.ts:9-17`，失败时降级明文文件）。API key 来源优先级（`utils/auth.ts`）：`apiKeyHelper`（settings 里配的执行脚本，`:123`）→ `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` 环境变量（`:126,134`）→ `--settings` 注入 → keychain。
- **token 带锁刷新**（`checkAndRefreshOAuthTokenIfNeeded`，`auth.ts:1427-1560`）：发现 accessToken 过期后，① 先清缓存重读磁盘（**别的进程可能已经刷新过了**）；② 真要刷新就抢文件锁（`lockfile.lock :1500`）防多进程并发刷新；③ 最多重试 5 次（`:1453`）；④ 订阅用户刷新时省略 scopes 以便服务端扩 scope（`:1544-1550`）。401/"token revoked" 时在重试层强制刷新（`withRetry.ts:246-252`）。
- **计费头与缓存作用域拆分**：每次请求的系统提示词第一块是 `x-anthropic-billing-header: cc_version=...; cc_entrypoint=...; cch=...`（`constants/system.ts:86-95`）。`splitSysPromptPrefix()`（`utils/api.ts:321-376`）把它从系统提示中摘出来、标记 `cacheScope: null`（不参与提示缓存），其余静态块用 `global`/`org` 缓存作用域——**换个入口（CLI/SDK）不会打破缓存**，这是精打细算的典型。

**例子**：你 `/login` 授权后 token 进了 macOS 钥匙串；一小时后 token 过期，`checkAndRefreshOAuthTokenIfNeeded` 拿文件锁静默续期——两个终端同时开着 Claude Code 也不会重复刷新，你完全无感知。

### 11.3 模型选择与 fallback：主角与替身

**是什么**：平时用主模型，Anthropic 服务器超载（HTTP 529）时自动换备用模型顶上，对话不中断。

**源码怎么实现**：

- **主模型优先级链**（`utils/model/model.ts:84-101` 的 `getMainLoopModel`）：① 会话中 `/model` 改的（`bootstrap/state.ts:838`）→ ② 启动参数 `--model` → ③ 环境变量 `ANTHROPIC_MODEL` → ④ settings 的 `model` → ⑤ 内置默认（Opus 4.6；3P 渠道 Sonnet 默认 4.5，因为上架滞后，`:103-133` 的 `@[MODEL LAUNCH]` 注释）。
- **fallback 指定**：`--fallback-model <model>`（`main.tsx:1000`，注意帮助文本注明"only works with --print"），且不允许和主模型相同（`:1337` 校验）。
- **触发与切换**：

```mermaid
sequenceDiagram
    participant 循环 as queryLoop
    participant 重试层 as withRetry
    participant API as Anthropic API

    循环->>重试层: 请求（主模型 Opus）
    重试层->>API: 试第 1 次
    API-->>重试层: 529 超载
    重试层->>API: 试第 2 次
    API-->>重试层: 529
    重试层->>API: 试第 3 次
    API-->>重试层: 529（MAX_529_RETRIES=3，withRetry.ts:54）
    重试层-->>循环: 抛 FallbackTriggeredError（:163-165）
    循环->>循环: currentModel 换成 fallbackModel<br/>清空本轮半成品 assistant 消息<br/>（补齐 'Model fallback triggered' 工具结果）<br/><i>query.ts:894-910</i>
    循环->>重试层: 用 Sonnet 重发整个请求
    重试层->>API: 请求（fallback 模型）
    API-->>重试层: ✅ 正常回复
```

注意细节：默认只对"非订阅用户的非定制 Opus"触发，设 `FALLBACK_FOR_ALL_PRIMARY_MODELS` 可放开（`:330-336`）；后台任务（摘要、标题生成）遇 529 直接丢弃不放大重试（`:319-327`）。
- **小模型分工**：`getSmallFastModel()`（`claude.ts:339-340`）——对话主题检测、命令描述等杂活用 Haiku 级别小模型，与主循环模型分离。好钢用在刀刃上。

**例子**：`claude -p --model opus --fallback-model sonnet "重构这个模块"`。高峰期 Opus 连续 3 次 529，循环静默切到 Sonnet 继续，你只在 transcript 里看到一条模型切换提示。

### 11.4 权限模式六档：给 AI 的"遛狗绳长度"

**是什么**：从"每一步都问你"到"完全放养"分几档，Shift+Tab 一键换档。

**六档语义**（`utils/permissions/PermissionMode.ts:42-91`）：

| 模式 | 语义 |
|---|---|
| `default` | 每次危险操作弹窗询问 |
| `plan` | 只许研究和出方案，不许动文件/跑命令；结束时用 ExitPlanMode 交方案给你批（`utils/permissions/filesystem.ts:1446` 起拦截写操作） |
| `acceptEdits` | 工作目录内的文件编辑自动放行（`filesystem.ts:1366`），其余仍询问 |
| `bypassPermissions` | 全部跳过询问（危险，需 `--dangerously-skip-permissions` 或在 UI 确认启用） |
| `dontAsk` | 不在 UI 循环中暴露（`getNextPermissionMode.ts:70-72`） |
| `auto` | 内部（ant-only）模式：转录分类器实时判断每个操作是否安全（feature flag `TRANSCRIPT_CLASSIFIER`，`PermissionMode.ts:80-90`） |

**Shift+Tab 循环**（键位 `keybindings/defaultBindings.ts:30-36`，Windows 无 VT 模式降级 `meta+m`）：

```mermaid
flowchart LR
    D["default"] --> A["acceptEdits"] --> P["plan"] --> B["bypassPermissions<br/>（若可用）"] --> D
    style P fill:#bde0fe
    style A fill:#caffbf
    style B fill:#ffc6ff
```

顺序定义在 `getNextPermissionMode.ts:34-79`；切换时 `cyclePermissionMode()`（`:88-101`）调 `transitionPermissionMode` 做上下文清理（如进 auto 模式剥离危险权限）；状态栏按模式变色（plan 蓝、accept 青、bypass 红，`PermissionMode.ts:26-30`）。

**例子**：让 Claude 帮你调研 bug 但怕它乱改：Shift+Tab 切到 `plan`，Claude 读代码写方案，最后 ExitPlanMode 弹"批准计划吗？"；批准后自动切到 `acceptEdits` 直接落地修改（`filesystem.ts:1450` 的模式建议）。

### 11.5 输出样式 outputStyles：给 Claude 换"人格面具"

**是什么**：输出样式直接改写系统提示词的开篇部分，让同一个模型以不同风格回答——教学式 vs 极简式，像换人格面具。

**源码怎么实现**：

- **内置样式**（`constants/outputStyles.ts:42-100`）：`default`（值为 null，不加料）、`Explanatory`（解释每个实现选择，附 `★ Insight` 块模板）、`Learning`（故意留 2-10 行小任务让你亲手写，`TODO(human)` 机制）。
- **自定义样式**：往 `~/.claude/output-styles/*.md` 或项目 `.claude/output-styles/*.md` 放 markdown 即可，frontmatter 支持 `name`、`description`、`keep-coding-instructions`（`loadOutputStylesDir.ts:41-62`）；插件也可通过 `output-styles/` 目录提供。
- **注入位置**：选中的样式拼成 `# Output Style: {name}\n{prompt}` 段落插进系统提示（`constants/prompts.ts:152-157`），总纲句随之改为"according to your Output Style below"（`:180`）。`keep-coding-instructions: true` 表示保留原有编程指令，只叠加风格。

**例子**：`/output-style Learning` 后，Claude 写 30 行代码时刻意留下核心函数，发一张"Learn by Doing"卡片让你补全 `TODO(human)` 部分——边用边学。

### 11.6 记忆系统：四层"大脑"

**是什么**：跨会话长期记忆（MEMORY.md 自动记忆）、项目公约（CLAUDE.md 系列）、当前会话笔记（Session Memory）、手动编辑器（/memory）四层。

**源码怎么实现**：

- **自动记忆（memdir）**：`memdir/memdir.ts`——每个项目一个记忆目录（默认 `~/.claude/projects/<项目路径>/memory/`），入口文件 `MEMORY.md` 硬上限 **200 行 / 25KB**，超出截断并附警告（`memdir.ts:35,37-40,59-95`）。Claude 自主把用户偏好、项目习惯写进该目录的 markdown 文件；启动时按相关性检索注入（`findRelevantMemories.ts`），注入条目带"新鲜度"前缀（`FileReadTool.ts:749` 的 `memoryFileFreshnessPrefix`）。你纠正 Claude 时，拒绝消息尾部还会追加"考虑存到记忆"的提示（`utils/messages.ts:177-191`）。
- **会话记忆（Session Memory）**：`services/SessionMemory/sessionMemory.ts:1-7`——后台 fork 一个子 agent 周期性把对话要点提炼成笔记，不打断主对话；用于 auto-compact 后恢复上下文。
- **CLAUDE.md 体系**：第五章已细讲（收集顺序、@导入、身份标签）；子目录 CLAUDE.md 在被读到的文件触发时按需注入（`attachments.ts:1710` + `FileReadTool.ts:848,870`）。
- **`#` 快捷记忆已不存在**：**明确说明**——本源码快照中没有 `#` 前缀输入模式：`inputModes.ts:31-33` 只识别 `!`（bash 模式），`PromptInputMode` 类型（`types/textInputTypes.ts:265-269`）也只有 bash/prompt 等。早期版本的 `#` 快捷追加记忆，已演进为 `/memory` 命令 + auto memory 体系——网上老教程里的 `#` 用法在这个版本会失效，注意版本差异。

**例子**：你随口说"我们团队 Commit 都用中文"，Claude 把这条写进 `memory/team-conventions.md` 并更新 `MEMORY.md` 索引；下周新开会话，这条偏好随相关记忆自动出现在系统提示里。

### 11.7 其他机制快览

| 机制 | 一句话 | 关键实现 |
|---|---|---|
| **Vim 模式** | 输入框支持 NORMAL/INSERT 切换、motion、operator、text object | 纯函数模块 `vim/motions.ts`、`operators.ts`、`textObjects.ts`、`transitions.ts`；状态类型 `textInputTypes.ts:255-260` |
| **语音输入** | 按住说话：连 Anthropic 的语音 WebSocket 做流式转写；**必须有 OAuth token**（走 claude.ai 端点，API key/Bedrock/Vertex 不可用） | `services/voiceStreamSTT.ts:1-36`；门槛 `voice/voiceModeEnabled.ts:18-24`（GrowthBook 急停）+ `:31-40 hasVoiceAuth` |
| **远程 / bridge** | 本机成为 claude.ai 网页/App 的"执行端"：轮询云端领任务、本地跑、回传结果；权限弹窗可同步给网页确认 | `bridge/bridgeMain.ts`；`remote/SessionsWebSocket.ts`、`remotePermissionBridge.ts` |
| **cost 追踪** | 每次 API 返回的 usage 按模型价目表折算美元累计，/cost 展示，跨会话持久化 | `cost-tracker.ts:3-27,146`；`utils/modelCost.js` |
| **遥测埋点** | 事件统一走 `logEvent()`，事件名形如 `tengu_*`；敏感字段用类型标记强制声明（`..._I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS`） | `services/analytics/`（Statsig/GrowthBook/Datadog/一方上报） |
| **快捷键系统** | 声明式键位：默认绑定表按 context 分块，用户可在 keybindings.json 覆盖；`ctrl+c`/`ctrl+d` 保留不可改绑 | `keybindings/defaultBindings.ts:37+`、`loadUserBindings.ts`、`parser.ts`/`match.ts` |
| **UDS 消息服务** | 启动时在 `$TMPDIR` 建 Unix Domain Socket，外部进程可写入消息注入对话；必须在 SessionStart hook 之前完成绑定 | `setup.ts:86-101`（feature `UDS_INBOX`，`--bare` 跳过） |
| **--worktree 隔离** | 会话在 git worktree（`.claude/worktrees/<slug>`）里运行，改动不碰主工作区；slug 有路径穿越防护 | `entrypoints/cli.tsx:247-261`；`utils/worktree.ts:52-55` |
| **SDK / headless** | `claude -p` 非交互模式：无 React 树，支持 `--output-format json/stream-json`；QueryEngine 把"一次查询"抽象成 SDK 与 CLI 共用的引擎 | `cli/print.ts`（5594 行，`:517` 注释）；`QueryEngine.ts:370,518` |
| **--bare** | 精简启动：跳过技能发现、UDS、遥测初始化等，要的就是快 | `loadSkillsDir.ts:654-675`、`setup.ts:86-88` |

> 🔍 **源码指路**：`src/utils/effort.ts:14-20,152-168`（effort）｜`src/utils/auth.ts:1427-1560`（token 刷新）｜`src/utils/api.ts:321-376`（计费头拆分）｜`src/utils/model/model.ts:84-101`、`src/services/api/withRetry.ts:54,163-165`、`src/query.ts:894-910`（模型与 fallback）｜`src/utils/permissions/PermissionMode.ts:42-91`、`getNextPermissionMode.ts:34-79`（权限模式）｜`src/constants/outputStyles.ts:42-100`（输出样式）｜`src/memdir/memdir.ts:35-95`、`src/services/SessionMemory/sessionMemory.ts`（记忆）

---

## 十二、记忆与遗忘：上下文压缩

### 12.1 为什么需要压缩

前面反复强调：**每轮请求都要带上全部历史**。而模型的"脑容量"（上下文窗口）有限，比如 20 万 token。一次大型重构，对话里塞满几百个文件内容、无数命令输出——很快撞顶。就算撞不到，历史越长：响应越慢、费用越高、模型还容易被无关旧信息干扰。

### 12.2 五层防线：每一圈循环开头的"瘦身检查"

Claude Code 在**每一圈循环开头**依次跑五道瘦身程序（`src/query.ts:379-648`）：

```mermaid
flowchart LR
    A["每圈循环开头"] --> B["① applyToolResultBudget<br>超大工具结果落盘存文件<br>历史里只留'摘要+路径'"]
    B --> C["② snip<br>剪掉冗余片段"]
    C --> D["③ microcompact<br>按 tool_use_id 清理<br>已完成使命的旧工具结果"]
    D --> E["④ context collapse"]
    E --> F{"⑤ 快到触发线了吗？"}
    F -->|没到| G["✅ 正常请求模型"]
    F -->|到了| H["autocompact 自动压缩"]
    H --> G
```

前四道是"局部减肥"（不损失信息或损失很小），第五道 **autocompact** 才是"大手术"。

### 12.3 触发线的数学：20 万窗口，19 万左右动手

触发线的计算（`src/services/compact/autoCompact.ts:62-76`）是道简单的算术，但每个数字都有讲究：

```ts
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000    // 警戒线余量
// ……
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3      // 熔断器
// 触发线 = effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
```

- **有效窗口 = 模型窗口 − min(最大输出, 20000)**：要给模型的"回答"预留空间，20 万窗口实际可用约 18 万；
- **触发线 = 有效窗口 − 13000**：18 万 − 1.3 万 ≈ **16.7 万左右就提前动手**（20 万窗口、默认输出配置下，体感上是在 19 万 token 之前触发）。为什么留 13000 余量？因为压缩摘要本身要占地方，而且"提前压缩"比"撞墙了再抢救"从容得多；
- **熔断器 = 连续失败 3 次放弃**：源码注释里写着血淋淋的教训——曾有 1279 个会话连续失败 50 次以上（最多 3272 次），全局每天浪费约 25 万次 API 调用，于是加了熔断。压缩失败烧钱，不能让重试失控；
- 环境变量 `DISABLE_AUTO_COMPACT` 可整体关停（此时撞到"窗口 − 3000"的 blocking limit 会直接报错，留你手动 `/compact` 的余地）。

### 12.4 压缩的方式：派一个"一次性分身"写读书笔记

最有意思的是压缩的做法：**不是粗暴砍掉前半截，而是派一个一次性分身 agent，把整段对话读一遍、写一份摘要**（`src/services/compact/compact.ts:387` 的 `compactConversation`，内部 `runForkedAgent` 复用同一个 query 循环，`querySource: 'compact'`、`maxTurns: 1` 只跑一轮）。

分身刻意保持**相同的 system prompt、相同的工具、相同的模型、相同的消息前缀**——为什么？为了命中 prompt cache：前缀相同意味着这次"写摘要"请求的大部分输入都能走缓存，省钱省时。

摘要写好后，`buildPostCompactMessages`（`compact.ts:330`）重组历史：

```mermaid
flowchart TB
    subgraph BEFORE["压缩前的历史（约 19 万 token）"]
        O1["你的问题 1"] --> O2["AI 回复 + 几十个工具结果"] --> O3["你的问题 2"] --> O4["更多工具结果……"]
    end
    subgraph AFTER["压缩后的历史（可能只剩 2 万）"]
        S["📝 摘要消息：分身写的'读书笔记'<br>（之前对话的要点总结）"]
        BND["✂️ compact_boundary 压缩边界标记"]
        R["边界之后的少量原始对话"]
        S --> BND --> R
    end
    BEFORE ==>|compactConversation| AFTER
```

从此 `getMessagesAfterCompactBoundary` 只取边界**之后**的内容加那份摘要——就像考试前把整本书浓缩成一页笔记，之后只看笔记和最后几章。20 万窗口下，一次压缩通常能把 19 万 token 压到两三万，对话因此可以无限继续下去（代价是边界前的细节丢了，只剩摘要里的要点）。

### 12.5 被动救场：撞墙之后的 413 抢救

如果预防没拦住、API 真的返回"内容太长"（413），还有**被动压缩（reactiveCompact）**：软件悄悄把错误**藏起来**（不让你看到吓人的报错，State 里的 `hasAttemptedReactiveCompact` 保证只试一次），紧急压缩一次再重发请求。所有手段都失败，才会告诉你"对话太长了，请 `/compact` 或开新对话"（`prompt_too_long` 终止）。

```mermaid
flowchart TD
    A["请求 API"] -->|413 prompt too long| B["withheld：错误先不显示"]
    B --> C["reactiveCompact 紧急压缩一次"]
    C --> D{"重试"}
    D -->|成功| E["✅ 用户无感知，循环继续"]
    D -->|失败| F["❌ 报 prompt_too_long<br>建议手动 /compact 或开新会话"]
    A -->|正常| G["继续主循环"]
```

> 🔍 **源码指路**：`src/query.ts:379-648`（五层防线）｜`src/services/compact/autoCompact.ts:62-76,262,343`（触发线与熔断）｜`src/services/compact/compact.ts:330,387`（边界重组与分身摘要）

---

## 十三、分身术与急刹车：子 Agent 与中断机制

### 13.1 子 Agent：一台发动机，多种跑法

任务很大时，模型可以调用 `Agent` 工具派"分身"去干子任务（比如"你把整个代码库搜一遍，找出所有用到这个函数的地方"）。架构上有个非常优雅的点（`src/tools/AgentTool/runAgent.ts:248`）：**子 agent 跑的就是主循环本身**——同一个 `query()` 函数（`runAgent.ts:748`），只是换了三样东西：一份自己的系统提示词、一个缩水的工具箱、一个身份标记（`agentId` 有值、`querySource: 'agent:...'`）。主 agent、子 agent、写摘要的分身，全是同一台发动机的复用——这也是为什么压缩分身能"顺便"命中 prompt cache。

任务系统（`src/Task.ts`）定义了七种任务类型：`local_bash` / `local_agent` / `remote_agent` / `in_process_teammate` / `local_workflow` / `monitor_mcp` / `dream`，ID 前缀分别是 `b/a/r/t/w/m/d`——你在界面上看到的任务编号前缀就暴露了它的类型。

### 13.2 前台 vs 后台：两种时序

子 agent 有两种跑法，区别全在"主循环等不等"：

```mermaid
sequenceDiagram
    participant 主 as 主循环
    participant 子 as 子 Agent

    Note over 主,子: 前台（同步）：主循环阻塞等待
    主->>子: 派任务（runAgent 同步调用）
    主-->主: ⏸️ 原地等待……
    子->>子: 自己跑完整的主循环（可能转很多圈）
    子-->>主: 最终结果作为 tool_result 返回
    主->>主: ▶️ 继续下一圈

    Note over 主,子: 后台（异步）：主循环立刻继续
    主->>子: 派任务（LocalAgentTask）
    子-->>主: 立刻返回 async_launched
    主->>主: ▶️ 继续干别的（可能又转了好几圈）
    子->>子: 后台独立运行
    子-->>主: 完成后往 messageQueueManager<br>塞一条 task-notification
    主->>主: 下一圈读到通知，继续决策
```

想象你让助理去查资料：前台模式是你坐在那等他回来；后台模式是你继续干自己的事，他查完了发微信告诉你。**什么时候用哪种？** 下一步依赖子任务结果时用前台（不等没法走）；只是想"顺手"做些旁路工作（如后台跑测试、监控）用后台。

还有一个实验性的 **coordinator 模式**（`feature('COORDINATOR_MODE')`）：主 agent 降级为只能用 Agent / SendMessage / TaskStop 三个工具的"协调者"——自己不干活，只派活和收结果，像项目经理。

### 13.3 ESC：一路通到网线的急刹车

你随时可按 ESC 中断 AI。这条"刹车线"的完整链路是：

```mermaid
flowchart LR
    A["⌨️ 你按 ESC"] --> B["defaultBindings.ts:66<br>escape → chat:cancel"]
    B --> C["useCancelRequest.ts:87<br>handleCancel"]
    C --> D["abortController.abort()"]
    D --> E["signal 一路传到<br>claude.ts 的 HTTP 层"]
    E --> F["🔌 TCP 连接真正掐断<br>立刻停止计费"]
    F --> G{"循环在哪个检查点发现？"}
    G -->|流式接收中（:1015）| H["补孤儿 tool_result<br>return aborted_streaming"]
    G -->|工具执行中（:1485）| I["补孤儿 tool_result<br>return aborted_tools"]
```

这个急刹车做得很彻底：

1. **HTTP 请求被真正掐断**——不是"装没看见结果"，是连接真的断了，立刻停止计费（6.4 节发起请求时传入的 `signal` 就是这根刹车线）；
2. **两个检查点优雅收尾**：流式接收中途断、或工具执行中途断，循环都会先做 6.6 节讲的"补孤儿 tool_result"（否则对话就报废了），再以 `aborted_*` 收场；
3. **空闲时按 ESC 另有妙用**：改为弹出消息队列里的下一条。

### 13.4 取消的层级：为什么 ESC 杀不死后台任务

取消信号是按"父子层级"传播的（`createChildAbortController`，父→子单向传播）：

```mermaid
stateDiagram-v2
    [*] --> 主循环Controller
    主循环Controller --> 前台子Agent: 共享同一个 controller
    主循环Controller --> 后台子Agent: 独立 controller
    主循环Controller --> 工具执行: 子 controller

    note right of 前台子Agent
        ESC → 父断子断，一起停
    end note
    note right of 后台子Agent
        ESC 停不了它（源码注释明说：
        background agents should survive
        when the user presses ESC）
        想杀它用 Ctrl+X Ctrl+K
    end note
```

为什么后台任务故意**不**跟着 ESC 死？因为反直觉：你按 ESC 是想停"当前正在刷屏幕的那个活"，不是想杀掉你十分钟前派出去、已经跑了半天后台测试的任务——误杀的代价远大于"多按一次专用快捷键"的麻烦。

> 🔍 **源码指路**：`src/tools/AgentTool/runAgent.ts:248,748`（子 agent 复用 query）｜`src/Task.ts`（七种任务类型）｜`src/keybindings/defaultBindings.ts:66`（ESC 绑定）｜`src/hooks/useCancelRequest.ts:87`（handleCancel）｜`src/query.ts:1015,1485`（两个中断检查点）

---

## 十四、总流程图：一图看完全程

把前面所有章节串起来，这就是你发出一段 prompt 之后的完整旅程：

```mermaid
flowchart TD
    subgraph 你的电脑["💻 你的电脑（Claude Code 软件）"]
        A["⌨️ 敲字回车<br><i>useTextInput → PromptInput → REPL</i>"] --> B["分流判断（纯字符串规则）：<br>空? exit? !shell? /命令?"]
        B -->|本地命令| C["⚙️ 本地执行<br><b>不发 AI</b>"]
        B -->|普通提问| D["📦 打包：系统提示词(+gitStatus)<br>+ &lt;system-reminder&gt;(CLAUDE.md+日期)<br>+ 全部历史 + 工具清单"]
        D --> E{"🔁 queryLoop while(true)"}
        E --> F["压缩防线五连<br>（落盘/snip/microcompact/collapse/autocompact）"]
        F --> G["流式请求模型<br><i>裸 stream，SSE 大 switch</i>"]
        G --> H{"有 tool_use？"}
        H -->|有| I["权限五关裁决<br>deny→ask→自查→模式→alwaysAllow→弹窗"]
        I -->|被拒| J["拒绝原因回喂模型<br>（模型自我纠正）"]
        I -->|放行| K["🔧 执行工具（边收边跑<br>只读并行/写操作串行）"]
        K --> L["tool_result 追加进历史"]
        J --> L
        L --> E
        H -->|没有| M["🎉 return completed<br>显示最终答案"]
        N["ESC 急刹车<br><i>signal 直达 HTTP 层</i>"] -.随时.-> G
        N -.随时.-> K
    end
    G -.->|HTTPS stream:true| O["☁️ Anthropic 服务器<br>Claude 大模型"]
    O -.->|SSE 事件流| G
    P["🤖 子 agent / 压缩分身<br>（复用同一个 query()）"] -.被 Agent 工具唤起.-> E
```

---

## 附录：源码导览索引

想亲自翻源码的话，这里是本文提到的关键文件地图：

| 主题 | 文件 | 看什么 |
|---|---|---|
| 程序入口 | `src/entrypoints/cli.tsx` | "分拣员" main()，`--version` 零加载 |
| 全局初始化 | `src/entrypoints/init.ts` | memoized init、预连 API |
| 会话初始化 | `src/setup.ts` | 工作目录、hooks 快照 |
| 界面渲染 | `src/ink/`、`src/components/`、`src/screens/REPL.tsx` | 终端 UI、输入框 |
| 按键处理 | `src/hooks/useTextInput.ts` | 回车的四种命运（:247） |
| 输入分流 | `src/utils/handlePromptSubmit.ts`、`src/utils/processUserInput/` | 空白丢弃（:188）、退出名单（:196）、shouldQuery |
| 斜杠命令 | `src/commands.ts` | findCommand 精确匹配（:688） |
| 上下文组装 | `src/constants/prompts.ts`、`src/context.ts`、`src/utils/api.ts` | getSystemPrompt（:444）、摆放位置（:437/:449） |
| 记忆文件 | `src/utils/claudemd.ts` | 收集顺序（:790）、@导入（:618）、身份标签（:1153） |
| **主循环（心脏）** | **`src/query.ts`** | queryLoop（:241）、while(true)（:307）、补孤儿（:123） |
| API 通信 | `src/services/api/claude.ts` | 裸流（:1822）、SSE switch（:1980-2303） |
| 工具定义 | `src/Tool.ts`、`src/tools.ts` | buildTool（:783）、getAllBaseTools（:193） |
| 工具执行 | `src/services/tools/toolExecution.ts`、`StreamingToolExecutor.ts` | runToolUse（:337）、并发纪律 |
| 权限系统 | `src/utils/permissions/permissions.ts`、`dangerousPatterns.ts` | 五关裁决（:1158） |
| 钩子系统 | `src/utils/hooks.ts`、`src/services/tools/toolHooks.ts` | spawn 执行、PreToolUse（:435） |
| 上下文压缩 | `src/services/compact/` | 触发线（autoCompact.ts:62）、分身摘要（compact.ts:387） |
| 子 Agent | `src/tools/AgentTool/runAgent.ts`、`src/Task.ts` | 复用 query()（:748）、七种任务 |
| 中断机制 | `src/hooks/useCancelRequest.ts`、`src/keybindings/defaultBindings.ts` | ESC 链路（:87/:66） |
| 工具图鉴（第八章） | `src/tools/`（39 个工具目录） | Bash 复合命令拆分（BashTool.tsx:447-465）、先读后写（FileEditTool.ts:281）、三标记默认值（Tool.ts:750-761） |
| 工具延迟加载 | `src/tools/ToolSearchTool/`、`src/utils/toolSearch.ts` | defer 名单（prompt.ts:62-108）、搜索打分（ToolSearchTool.ts:132-216） |
| MCP 工具包装 | `src/services/mcp/client.ts` | fetchToolsForClient（:1743-1832）、annotations→三标记（:1795-1809） |
| 斜杠命令（第九章） | `src/commands.ts`、`src/commands/` | COMMANDS 注册（:258）、合并顺序（:447-470）、安全名单（:619/:651） |
| 命令分发 | `src/utils/processUserInput/processSlashCommand.tsx` | getMessagesForSlashCommand（:525）、fork 执行（:62）、prompt 命令生命周期（:827） |
| 代表命令 | `src/commands/conversation.ts`、`commands/commit.ts`、`commands/init.ts` | /clear 软重启（:49）、预执行+白名单（:16-19）、/init 剧本（:226） |
| 自定义命令/技能加载 | `src/skills/loadSkillsDir.ts`、`src/utils/promptShellExecution.ts` | frontmatter（:185-265）、命名空间（:523-552）、shell 预执行红线（:371-374） |
| immediate 命令 | `src/screens/REPL.tsx`、`src/utils/immediateCommand.ts` | 绕过排队（:3158-3184） |
| 插件生态（第十章） | `src/utils/plugins/pluginLoader.ts`、`marketplaceManager.ts` | 目录结构探测（:1392-1398）、稀疏克隆（:803/:1034） |
| 技能渐进披露 | `src/utils/attachments.ts`、`src/skills/bundledSkills.ts` | 清单附件（:2661）、bundled 技能注册（:53） |
| MCP 生态 | `src/services/mcpServerApproval.tsx`、`services/mcp/types.ts` | 启动审批（:13-40）、连接状态机（:183-248） |
| effort 机制（第十一章） | `src/utils/effort.ts`、`src/utils/thinking.ts` | 优先级链（:152-168）、adaptive vs budget（:10-13） |
| 认证与 token | `src/services/oauth/index.ts`、`src/utils/auth.ts`、`src/utils/secureStorage/` | PKCE（:14-120）、带锁刷新（:1427-1560）、钥匙串 |
| 模型与 fallback | `src/utils/model/model.ts`、`src/services/api/withRetry.ts` | 优先级链（:84-101）、FallbackTriggeredError（:163-165） |
| 权限模式 | `src/utils/permissions/PermissionMode.ts`、`getNextPermissionMode.ts` | 六档语义（:42-91）、Shift+Tab 循环（:34-79） |
| 输出样式 | `src/constants/outputStyles.ts`、`outputStyles/loadOutputStylesDir.ts` | 内置样式（:42-100）、注入位置（prompts.ts:152-157） |
| 记忆体系 | `src/memdir/memdir.ts`、`src/services/SessionMemory/sessionMemory.ts` | MEMORY.md 上限（:35-95）、会话笔记 |
| 功能快览 | `src/vim/`、`src/voice/`、`src/bridge/`、`src/cost-tracker.ts`、`src/cli/print.ts` | vim/语音/远程/cost/SDK 各机制入口 |

> **关于这份源码的小知识**：这是一份内部构建版本的 TypeScript 源码快照（带 sourcemap 还原），函数名基本保留，注释详尽。你可能会看到 `feature('XXX')` 这样的编译期开关和少量缺失文件（例如部分 bash 权限模块在本快照中被合并），都属于构建系统的正常痕迹，不影响理解主流程。

---

## 结语：三句话带走

1. **大模型只会说话，Agent 软件是它的手和眼睛**——Claude Code 的全部工作就是"把模型的意图翻译成操作，把操作的结果翻译回文字"。
2. **心脏是一个 `while` 循环**：请求模型 → 有 tool_use 就执行并追加结果 → 再请求 → 直到模型不再要工具，答案就出来了。压缩、流式、子 agent、中断，全是围绕这个循环的配套设施。
3. **安全靠架构而非自觉**：输入分流的死规则、权限的五关裁决、hooks 快照、ESC 直达网线的急刹车，都是软件层面的硬约束——模型想做什么都得"申请批准"，而批准权在你。

理解了这三点，你再看市面上任何 AI Agent 产品（Cursor、Devin、各种自动化助手），都会发现它们跳不出这个基本框架——差别只在工具箱的丰富程度、循环的精细程度、和安全机制的严格程度。

### 学完路线建议：接下来去哪儿练手

读完前十一章（机制主线 + 工具/命令/生态/特性四本图鉴），最好的巩固方式是动手。按从易到难的路线：

1. **写一个自己的斜杠命令**（9.4 节）：在 `.claude/commands/` 放一个 `.md`，用上 `$ARGUMENTS` 和 `` !`shell` `` 预执行，十分钟就能收获第一个"私人快捷键"；
2. **升级成一个技能**（10.2 节）：把它搬进 `.claude/skills/<名>/SKILL.md`，写上 `when_to_use` 和 `allowed-tools`，体验"渐进披露"如何让模型自己找到它；
3. **装一个 MCP 服务器**（10.3 节）：在项目 `.mcp.json` 里登记一个（比如 GitHub 官方服务器），走一遍启动审批，然后看工具列表里多出来的 `mcp__*` 工具——你会真切体会到"USB-C"的即插即用；
4. **逛一圈官方插件市场**（10.1 节）：`/plugin marketplace add anthropics/claude-plugins-official`，装一个插件，再回头读它的 `plugin.json` 和目录结构，看看别人是怎么打包工作流的；
5. **跑一次 /init 并逐行审阅产物**（9.3 节）：让它为你的项目生成 CLAUDE.md，用"删掉这行会不会让 Claude 犯错"的标准自己改一遍——这是把第五、十一章知识用起来的最快方式。

做完这五件事，你就不只是"读过"Claude Code 的工作原理，而是真正在按它的设计哲学使用它了。
