This lecture is based on the actual
claude-codesource (a snapshot from around March 2026). Intended reader: a beginner in computer science — you do not need to write code; you only need to know that "a program is instructions executed one step at a time." How to read: the text contains many mermaid diagrams; use a reader that renders mermaid (VS Code + the Markdown Preview Mermaid Support plugin, Typora, Obsidian). Every important claim is taggedfile:line, so you can follow the trail into./claude-code/src/.Compared with the primer, every chapter here goes into real source: quoted snippets with line-by-line commentary, the engineering trade-off of "why this design," a diagram for each mechanism, and multiple examples of different fates for each class of scene.
🗺️ Companion architecture diagram: this project has a dedicated figure in the seven-agent architecture gallery — the full timing of one turn (permission is a cross-cutting gate; context is reassembled every turn). Every figure in the gallery first states "what question it answers" and "the load-bearing thesis," and has been through three rounds of review and render acceptance.
Contents
- Start with the idea: what is an AI agent?
- The map: five layers and the data flow of one request
- The journey begins: what happens at startup
- The moment you press Enter: how input is routed
- Packing the suitcase: how a request to the model is assembled
- The heart: the agent loop (the most important chapter)
- The model's "hands": the tool system and the permission gate
- A field guide to the tools: the model's 40 weapons, one by one
- A field guide to slash commands: how 60+ shortcuts work
- Ecosystem components: what plugins, skills, and MCP each are
- Feature teardown: effort, login, model switching, …
- Memory and forgetting: context compaction
- Clones and the emergency brake: sub-agents and interrupt
- The full picture: one diagram of the whole path
- Appendix: source map
1. Start with the idea: what is an AI agent?
1.1 The model itself can only "talk" — and it talks with no memory
Start with two common misconceptions.
Misconception 1: AI is a smart program that can do anything. In fact a large language model (LLM) does exactly one thing: it reads a stretch of text, then continues writing text. Picture a super-erudite person locked in a room — they have read every book in the world, but they have no hands, no eyes, no telephone; the only way you communicate is by sliding a note under the door.
Misconception 2: AI remembers what you said. In fact the model has no memory at all. Every time you send a message, the software has to resend the entire chat log from the first sentence. "It remembers" is the software remembering for it. That point will come up again and again — it is the key to understanding "why longer sessions get slower and more expensive" and "why we need compaction."
Both misconceptions point at the same conclusion: a model by itself cannot "edit a source file" for you — it can only tell you "you should change line 3 to xxx." The act of changing the file is something it cannot do.
1.2 Agent = brain + hands + a loop
An agent is the software system that gives that "person in the room" hands and eyes:
| Metaphor | What it is in Claude Code | Source location |
|---|---|---|
| 🧠 Brain | The Claude model (remote API call) | src/services/api/claude.ts |
| ✋ Hands | Tools: read/write files, run commands, search | src/tools/ (about 40) |
| 👀 Eyes | The results those tools return (tool_result) |
src/services/tools/toolExecution.ts |
| 🔁 Nervous system | The main loop: feed results back to the brain so it can decide the next step | queryLoop in src/query.ts |
| 🛡️ Guard | The permission system: dangerous actions need your consent first | src/utils/permissions/ |
The core idea of an agent fits in one sentence:
The model talks → the software acts for it → the result of that action is told back to the model → the model talks again → until the job is done.
1.3 What "the model wants a tool" actually looks like in the data
This is the place beginners most often find "mystical." We take it apart with data. The model does not actually pick up a phone; it generates a special structure in the reply stream, called a tool_use block, roughly:
{
"type": "tool_use", // 块的类型:这是一次工具调用
"id": "toolu_01ABC...", // 这次调用的唯一编号(后面配对要用)
"name": "Bash", // 要用哪个工具
"input": { "command": "ls src" } // 给工具的参数
}
When the software receives that block, it knows "the model wants to act," and acts for it. The result is wrapped in another special structure (a tool_result block) and sent back:
{
"type": "tool_result",
"tool_use_id": "toolu_01ABC...", // 注意:用 id 配对,标明这是哪次调用的结果
"content": "a.ts\nb.ts\nc.ts" // 真实的命令输出
}
The whole agent loop, said plainly, is the software acting as "translator + runner": turn the model's textual intent into a real operation, then turn the result of that operation back into text. And that id pairing mechanism is the source of a key design in chapter 13 ("orphan tool results must be patched with a fake reply").
1.4 A minimal example
Suppose you say: "look at the files in src, then tell me which file is largest." Inside the software the conversation looks like this:
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)
Watch: the model speaks three times. The first two times are tool_use; the third has no tool_use — "no tool call" is the loop's stop signal. Everything else is there to make the loop safer, faster, and cheaper in tokens.
2. The map: five layers and the data flow of one request
Before the details, a map. Claude Code, outside in, is five layers:
flowchart TB
subgraph L1["🖥️ 第 1 层:界面层(终端 UI)"]
UI["输入框 / 文本渲染 / 权限弹窗
src/components · src/screens/REPL.tsx · src/ink"]
end
subgraph L2["🚦 第 2 层:输入分流层"]
ROUTER["判断输入是斜杠命令 / !shell / 普通提问
src/utils/handlePromptSubmit.ts · processUserInput.ts"]
end
subgraph L3["📦 第 3 层:上下文组装层"]
CTX["系统提示词 + CLAUDE.md + git 状态
src/constants/prompts.ts · src/context.ts · src/utils/api.ts"]
end
subgraph L4["🔁 第 4 层:Agent 主循环(心脏)"]
LOOP["请求模型 → 执行工具 → 追加结果 → 再请求
src/query.ts · src/services/tools/"]
end
subgraph L5["🌐 第 5 层:API 通信层"]
API["流式 HTTP / SSE 解析 / 重试 / 中断
src/services/api/claude.ts"]
end
UI --> ROUTER --> CTX --> LOOP --> API
LOOP -.->|调用| TOOLS["🔧 工具箱:Bash / Read / Write / Edit / Grep ...
src/tools/"]
API -.->|HTTPS| CLAUDE["☁️ Anthropic 服务器上的 Claude 模型"]
- Layer 1 (UI): the input box you see in the terminal, the typing effect, the "allow this command?" dialog — all rendered in the terminal with React (a heavily modified Ink library lives in
src/ink/). Writing a terminal UI in React is an interesting choice: developers can reuse mature component thinking to manage complex UI state. - Layer 2 (routing): after you press Enter, the software first decides whether the text is for the model, or a local command. This layer's rules are surprisingly naive (chapter 4).
- Layer 3 (packing): once it is going to the model, pack the system prompt, memory files, git status, and other "background material." This layer's core tension is "more information for the model is better" versus "tokens are expensive and slow" (chapter 5).
- Layer 4 (the loop): the real heart. The most precise engineering in the whole product is piled here (chapters 6, 12, 13).
- Layer 5 (transport): talk to Anthropic's servers. The keyword is "streaming" — the server writes a bit and sends a bit, which is why you see the answer "typed" one character at a time.
For one complete request, data flows across the layers like this:
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 的那次回复)
Next we follow one prompt from birth to answer, layer by layer.
3. The journey begins: what happens at startup
Between typing claude in the terminal and seeing the input box, the software quietly does a lot. First the full flow:
flowchart LR
A["敲下 claude"] --> B["分拣员 cli.tsx
特殊参数走快速通道"]
B --> C["main.tsx
解析命令行参数"]
C --> D["init.ts 全局初始化(只做一次)
配置/安全环境变量/CA证书/预连API"]
D --> E["setup.ts 会话初始化
定工作目录/hooks 配置快照"]
E --> F{"目录信任对话框
❓信任此文件夹吗?"}
F -->|信任| G["后台预热上下文
预读 git 状态 + CLAUDE.md"]
G --> H["渲染交互界面
输入框出现!"]
F -->|拒绝| I["退出"]
3.1 The sorter: why --version is lightning-fast
The program entry main() in src/entrypoints/cli.tsx is a parcel sorter. Look at this real source (around cli.tsx:27-44):
/**
* 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') 动态加载主程序
}
Line by line: the file-header comment states the design intent outright — "the --version fast path has zero imports beyond this file." The main program has a thousand modules; loading them all takes hundreds of milliseconds; console.log of a version string takes a few milliseconds. So the sorter looks at the arguments first: a simple request like --version prints and immediately returns; only the default path uses await import('../main.js') to dynamically load the main program. That is why claude --version comes back instantly, while claude needs a moment to start.
Why this design? A CLI tool's "felt speed" is largely decided by the response time of the simplest commands (imagine how annoyed you would be if git --version stalled for 2 seconds). Turning every heavy module into a dynamic import, trading "do less at startup" for "common paths are instant," is a classic CLI trade-off.
3.2 Global init: "opening the shop," done once
After entering the main program, init() in src/entrypoints/init.ts runs. Notice how it is defined (init.ts:57):
export const init = memoize(async (): Promise<void> => {
// 读配置、设置安全相关环境变量、处理企业 CA 证书……
// (init.ts:159)preconnectAnthropicApi()
})
memoize means: no matter how many places in the code call init(), the function body actually runs once; later calls return the first result. One especially interesting thing in init is preconnectAnthropicApi(): do a TLS handshake with Anthropic in advance. Like dialing the phone and leaving it off-hook, so when you actually speak you do not wait for the ring — that can save 100–200 milliseconds. Why put it in init rather than at the first request? Because the handshake can run in parallel with you "looking at the trust dialog, typing the first question" — free acceleration.
3.3 Session init: the hooks "anti-tamper" snapshot
src/setup.ts does session-level init: determine the current working directory (setCwd), handle --worktree and similar flags. One security detail: user-configured hooks (hook scripts, chapter 7) are snapshotted here. Why? Hooks are config that "automatically runs a shell command on your machine at certain moments," which is powerful; if someone quietly changed the config file mid-session (say a malicious script), the snapshot guarantees this session keeps using the config you confirmed at startup, against mid-session tampering.
3.4 The trust dialog: a real security boundary
The first time you run in a folder, it pops "do you trust this directory?" (showSetupScreens in src/interactiveHelpers.tsx). This is not a ceremonial welcome page; it is a security boundary: a project directory can hide other people's config (hooks, CLAUDE.md); a malicious repo can use those to make your machine run dangerous commands. Those project-level configs only take effect after you click "trust."
And the moment trust is granted, the software immediately in the background prefetches getSystemContext() (git status) and getUserContext() (CLAUDE.md). Both functions are memoize-cached — prefetch equals "heat the cache in advance," so your first question's response is faster. Same idea as the 3.2 API preconnect: any preparation that can be done early is never left until the user speaks.
🔍 Source pointers:
src/entrypoints/cli.tsx:27-44(the sorter) |src/entrypoints/init.ts:57,159(memoized init and preconnect) |src/setup.ts(hooks snapshot) |src/interactiveHelpers.tsx(trust dialog and warmup)
4. The moment you press Enter: how input is routed
The input box is up. You type a stretch of text and press Enter. From that second to "the request goes out" sits a chain much longer than you would guess.
4.1 From keypress to text: a journey from bytes to characters
Every key you type takes a journey:
flowchart LR
A["键盘按键"] --> B["终端把按键变成
原始字节流(stdin)"]
B --> C["App.tsx parseMultipleKeypresses
ANSI 转义序列状态机
把字节解析成'结构化按键'"]
C --> D["useInput hook 分发"]
D --> E["useTextInput.ts
文本状态机:更新内容与光标"]
E --> F{"是回车键?"}
F -->|普通字符| E
F -->|回车| G["onSubmit(完整文本)
文本正式离开输入框"]
Step 3 is worth explaining: in a terminal, arrow keys, Enter, and Ctrl combinations are not ordinary characters; they are "escape sequences" starting with ESC (pressing ↑ actually delivers 3 bytes ESC [ A). So the software needs a state machine to restore that byte stream into a structured event like "the user pressed up-arrow."
Enter handling is handleEnter in src/hooks/useTextInput.ts:247:
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) // 以上都不是 → 才是真正的提交
}
Line by line: Enter does not necessarily equal submit. The first three branches all handle "the user actually wanted a newline" (trailing \, Shift+Enter, the Apple Terminal special case); only after all of those are excluded is onSubmit called. That is why you can write multiline text in the input box with \ plus Enter.
4.2 A UI gate: another sieve before onSubmit
After text leaves useTextInput, it still has to pass the local onSubmit in PromptInput.tsx (around :984), which is responsible for several things "only the UI layer can judge":
- Do not submit while a footer dialog is open (you are looking at the help page; Enter is for the dialog);
- Ghost-text accept: that gray "guess what you wanted to type" line in the input box — pressing Enter may be accepting it, not submitting;
- @teammate direct send: in multi-agent collaboration,
@name contentis routed directly to that member; - Empty input is dropped.
All of that passing, onSubmit in src/screens/REPL.tsx:3142 takes over — and from here on it is the real "sorting desk."
4.3 The sorting desk: pure string matching, zero model involvement
Many people assume the sorting desk "intelligently understands" intent. In fact it uses a few hard-coded string rules, all the most naive comparison.
Rule 1: look at the first character (src/components/PromptInput/inputModes.ts:16):
export function getModeFromInput(input: string): HistoryMode {
if (input.startsWith('!')) { // 首字符是 ! → bash 模式
return 'bash'
}
return 'prompt' // 否则 → 候选发给 AI
}
Slash commands are the same idea: inputString.startsWith('/') in processUserInput.ts.
Rule 2: blank input is dropped (src/utils/handlePromptSubmit.ts:188): if (input.trim() === '') return — you typed a few spaces and hit Enter, nothing happens.
Rule 3: exit words are a hard-coded list (handlePromptSubmit.ts:196):
if (['exit', 'quit', ':q', ':q!', ':wq', ':wq!'].includes(input.trim())) {
// 改写成 /exit 命令,走正常的退出流程(还会弹反馈对话框)
}
Note it is .includes() — the input must exactly equal one of the words. "help me exit this infinite loop" in natural language does not match and is sent to the model as usual. (:q and :wq are easter eggs for Vim muscle memory.)
Rule 4: slash commands are a dictionary lookup, also exact match (src/commands.ts:688):
export function findCommand(commandName: string, commands: Command[]) {
return commands.find(
_ =>
_.name === commandName || // 名字精确相等
getCommandName(_) === commandName || // 或全名精确相等
_.aliases?.includes(commandName), // 或命中别名列表
)
}
A miss returns Unknown command: xxx honestly. It will never "guess" you meant a different command.
Why design it this "dumb"? A deliberate engineering trade-off:
- Determinism: whether your input goes to the model involves privacy and cost, and must be 100% predictable. If you used "smart classification," a mis-fire could upload a private aside;
- Speed: string comparison is nanoseconds, zero latency, zero tokens;
- Debuggability: the rules are in the code; behavior is fully reproducible.
The place that actually "understands" your input is after sorting. The sorting desk is a bouncer who follows the book: it does not know people, it only checks IDs.
4.4 Seven inputs, seven fates
String the rules together and watch where seven real inputs go (this is the most important figure in the chapter):
flowchart TD
INPUT["你敲下的文本"] --> Q1{"trim 后为空?"}
Q1 -->|如只敲了空格| DROP["🗑️ 丢弃
handlePromptSubmit.ts:188"]
Q1 -->|否| Q2{"精确等于 exit/quit/:q/:q!/:wq/:wq!?"}
Q2 -->|是| EXIT["本地执行 /exit,退出程序
不发 AI"]
Q2 -->|否| Q3{"! 开头?"}
Q3 -->|是| BASH["💻 bash 模式:本地直接执行 shell
输出只给你看
不发 AI(shouldQuery:false)"]
Q3 -->|否| Q4{"/ 开头?"}
Q4 -->|是| CMD{"findCommand 查命令表"}
CMD -->|local-jsx 型
如 /config| LOCALUI["⚙️ 打开本地界面
不发 AI"]
CMD -->|local 型
如 /clear| LOCAL["⚙️ 本地执行(清空对话)
不发 AI"]
CMD -->|prompt 型
如 /commit、技能| PROMPT["📝 展开成一段预制提示词
发给 AI"]
CMD -->|查无此令| UNKNOWN["提示 Unknown command
不发 AI"]
Q4 -->|否| Q5{"AI 正在忙吗?
queryGuard.isActive"}
Q5 -->|在忙| QUEUE["📥 进 messageQueueManager 排队
当前回合结束后再处理"]
Q5 -->|空闲| TEXT["✉️ 包装成用户消息
发给 AI(shouldQuery:true)"]
Walk the examples one by one:
!ls -la→ bash mode. The command runs on your machine, the output is shown to you,shouldQuery: false(processUserInput.ts:206) — not one byte is sent to the model. For when you want to run a command yourself, quickly./clear→ alocalslash command: locally clear conversation history, not sent to the model. That is why/cleardoes not consume API quota./config→local-jsx: render a settings UI in the terminal for you to click, also not sent to the model./commit→prompttype: the software expands the command into a prefabricated prompt (roughly "look at git status, write a commit message and commit"), and that expanded text is what goes to the model. Skills take this path too.help me fix a bug→ ordinary question: wrapped as a user message and sent to the model,shouldQuery: true.- A message you sent while the model was busy → into the
messageQueueManagerqueue. When the current turn ends, queued messages are taken out and run through the sorting pipeline again. exitor:q→ local exit. Note it is rewritten as/exitand takes the normal command path, not a crudeprocess.exit— so exit can still clean up and pop a feedback dialog.
Two more points that often confuse:
- Does history recalled with ↑ get sent to the model? No. That history lives locally in
~/.claude/history.jsonl(src/history.ts) and only serves "up-arrow recall." It is a different object from the conversation history sent to the model. - Before send there is still a user-installed security checkpoint: the UserPromptSubmit hook (
processUserInput.ts:174-264). If you have a hook script in config, it can intercept your input here, rewrite it, or append extra material. Chapter 7 covers the hook mechanism.
4.5 Attachments: how @file is stuffed in
If the input contains @src/main.ts, or your IDE has a selection, those are collected at this stage by getAttachmentMessages in src/utils/attachments.ts as "attachment messages" and sent after your question. Pasted images are also compressed here and turned into image blocks the model can read. Skill-listing attachments (the "progressive disclosure" of chapter 5) take this channel too.
🔍 Source pointers:
src/hooks/useTextInput.ts:247(Enter handling) |src/components/PromptInput/PromptInput.tsx:984(UI gate) |src/screens/REPL.tsx:3142(REPLonSubmit) |src/utils/handlePromptSubmit.ts:188,196(blank drop, exit list) |src/components/PromptInput/inputModes.ts:16(!test) |src/commands.ts:688(findCommand) |src/utils/processUserInput/processUserInput.ts:174-264,446(UserPromptSubmit hook, bash mode) |src/utils/attachments.ts(attachment collection)
5. Packing the suitcase: how a request to the model is assembled
Your question has been confirmed as "send to the model." You thought the only thing going over was "help me see whether this function has a bug"? In fact every request is a large parcel:
flowchart TB
subgraph PACKAGE["📦 发往 Anthropic API 的一个完整请求"]
direction TB
SYS["1️⃣ system 字段:系统提示词
静态段(角色/规范/工具说明)+ 动态段(技能清单/环境信息)
末尾追加 gitStatus"]
MSG0["2️⃣ 对话第一条 user 消息(isMeta 隐身)
<system-reminder> 包裹的 CLAUDE.md + 今天日期"]
HISTORY["3️⃣ 对话历史
你说过的话 + AI 每次回复 + 每次工具结果,原封不动全带"]
NEW["4️⃣ 你的新消息 + @附件/图片"]
TOOLS["5️⃣ tools 参数:约 40 个工具的说明书
名字 + 用途描述 + 参数 JSON Schema"]
end
This material is collected on three parallel paths (source: getSystemPrompt at src/constants/prompts.ts:444, getUserContext/getSystemContext in src/context.ts):
getSystemPrompt→ the system prompt (piece 1);getUserContext→CLAUDE.md+ today's date (piece 2);getSystemContext→ git status (appended at the end of piece 1).
The three pieces are fetched in parallel (all caches warmed at startup), then two functions in src/utils/api.ts "seat them" in the request. Piece by piece.
5.1 The system prompt: Anthropic's "employee handbook" for the model
The system field on every request is Anthropic's employee handbook for the model, in two segments:
- Static segment: role definition ("you are Claude Code, a programming assistant"), behavioral rules ("you must read a file before editing it," "keep answers concise"), tool-use rules, dozens of clauses. The static segment is basically the same for every user — that is deliberate. An identical prefix hits the API's prompt cache (the cached portion is not billed / billed less), so
getAllBaseTools()(src/tools.ts:193) carries a comment warning that changes must stay in sync with the cache config. - Dynamic segment: the currently available skill listing, a memory summary, environment facts (OS / shell / model version).
5.2 CLAUDE.md collection rules: five sources, layered
CLAUDE.md is not "read one file." It is a collection pipeline with a strict order (getMemoryFiles at src/utils/claudemd.ts:790), collecting 5 sources in sequence and concatenating all of them:
flowchart TD
A["开始收集记忆文件"] --> B["1️⃣ Managed(企业管控)
/etc/claude-code/CLAUDE.md + 管控版 rules
最先加载:公司 IT 下发的强制规范"]
B --> C["2️⃣ User(用户全局)
~/.claude/CLAUDE.md + ~/.claude/rules/*.md
你个人对所有项目的偏好"]
C --> D["3️⃣ Project + Local(项目)
从当前目录逐级向上走到根目录
每层找 4 个目标:"]
D --> E["· CLAUDE.md(项目规范,提交进 git)
· .claude/CLAUDE.md(另一种放法)
· .claude/rules/*.md(拆成多条的规则)
· CLAUDE.local.md(私人项目笔记,不提交 git)"]
E --> F["4️⃣ --add-dir 额外目录
需环境变量开启,默认关闭"]
F --> G["5️⃣ 自动记忆 / 团队记忆
实验特性,feature gate 控制"]
G --> H["全部拼成一大段文字"]
A few details worth knowing:
- Walking upward means "nearest overlay": if you start in
projectA/src/utils, the root, projectA, src, and utils each contribute theirCLAUDE.md— files closer to you are more specific; all of them apply; none overwrite each other; the model decides which clause is more relevant. - Every file gets an "identity tag" (
getClaudeMds,claudemd.ts:1153): when concatenated, each file is preceded by its path and type, e.g.(project instructions, checked into the codebase),(user's private global instructions for all projects)— so the model knows which clause is a company mandate and which is your private note, and treats them differently. @includesyntax and safety gates (processMemoryFile,claudemd.ts:618): aCLAUDE.mdcan write@path/file.mdto include another file; the software expands recursively, with four gates — at most 5 levels (MAX_INCLUDE_DEPTH); only a text-extension allowlist (so you cannot stuff images or PDFs into memory); project-level files by default cannot include files outside the project directory (so a malicious repo cannot steal your private files with@~/.ssh/id_rsa); every path goes into aprocessedPathsset for dedup (including symlink canonicalization). Per-file cap 40000 characters (MAX_MEMORY_CHARACTER_COUNT).- Can be turned off entirely: env
CLAUDE_CODE_DISABLE_CLAUDE_MDSor--baremode skips auto-discovery (src/context.ts:165).
5.3 Placement rules: instructions into system, memory disguised as the first user message
Once the material is collected, where it sits in the request is deliberate (src/utils/api.ts). Two real snippets:
// 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,
]
}
Line by line: appendSystemContext formats git status as key: value and concatenates it at the end of the system-prompt array — git status is "environment state," so sitting with instructions is most reasonable. prependUserContext wraps CLAUDE.md as a <system-reminder> user message and inserts it at the head of conversation history.
Why not put it in the system field too? Three design tricks:
isMeta: truestealth flag — the model can see it; your terminal UI does not display it; the dialog is not polluted;IMPORTANT: this context may or may not be relevant— tells the model clearly "this is background, not a question," so it does not treatCLAUDE.mdas something to answer;- Cache-friendly: git status and
CLAUDE.mdare read once per session (memoize) and resent identically every turn, which is exactly what hits the prompt cache, saving money and time — which is also why we "warm" them at startup (chapter 3).
5.4 Git status: a repo snapshot "as of session start"
The git information getSystemContext collects includes: current branch, main-branch name, user.name, git status --short (truncated past 2000 characters, MAX_STATUS_CHARS = 2000 at context.ts:20), the last 5 commits. The content explicitly notes this is "a snapshot as of session start" — because collection is memoized, it will not refresh mid-session; the software chooses to tell the model honestly "this may be stale," rather than spend hundreds of milliseconds re-running git every turn. Between accuracy and speed, it chose speed + an honest label.
🔍 Source pointers:
src/constants/prompts.ts:444(getSystemPrompt) |src/context.ts:20,165(git truncation, kill switch) |src/utils/claudemd.ts:618,790,1153(@include, collection order, identity tags) |src/utils/api.ts:437,449(appendSystemContext/prependUserContext)
6. The heart: the agent loop (the most important chapter)
This chapter is the soul of the software. Understand it and you understand how every AI agent works.
6.1 The skeleton of the loop: a while (true)
The source is src/query.ts. The entry query() (:219) is only a shell; the real heart is queryLoop (:241). The core structure is surprisingly simple — a while (true) infinite loop plus a State struct held across turns (:265-284):
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
}
}
The source comments also hide an evolution: a checkpoint is named query_recursive_call (:1714); a historical comment says this loop used to be recursive, then was rewritten as while + State. Why change? Recursion deepens the call stack one frame per turn; after dozens of tool calls the stack is deeper and deeper, debug traces are ugly, and you cannot do unified cleanup at each "continue point." After while + State, all cross-turn state lives in one struct; the 7 continue points uniformly write state = { ... }; behavior is obvious. A typical trajectory of "write it correctly first, then refactor it clear."
6.2 What the loop looks like: one overview figure
flowchart TD
START(["带着打包好的请求进入循环"]) --> COMPACT["① 压缩防线
第十二章细讲"]
COMPACT --> CALL["② deps.callModel 流式请求
src/services/api/claude.ts"]
CALL --> COLLECT["③ 边收边收集 tool_use 块
needsFollowUp 是唯一核心循环信号"]
COLLECT --> CHECK{"有没有
工具调用?"}
CHECK -->|"没有"| DONE["🎉 跑 stop hooks 后
return completed(:1357)"]
CHECK -->|"有"| EXEC["④ 执行工具
第七章细讲"]
EXEC --> APPEND["⑤ state.messages =
[...旧历史, ...assistant回复, ...toolResults]
query.ts:1716"]
APPEND --> COMPACT
In one sentence:
As long as the model still wants a tool, the loop does not stop. The moment the model stops asking for tools, the answer is out.
6.3 A full demo: "find every TODO in the project and summarize"
A real three-turn trace. You say: "find every TODO comment in the project and summarize."
| Turn | History sent to the model (accumulating) | The model's reply | What the software does |
|---|---|---|---|
| Turn 1 | employee handbook + git status + your question | tool_use: Grep(pattern="TODO") |
run the search; dozens of matches appended to history |
| Turn 2 | all of turn 1 + Grep results | tool_use: Read(file 1) and Read(file 2) (in parallel) |
two read-only tools run together; file contents appended to history |
| Turn 3 | all of the first two turns + file contents | plain text: "found 23 TODOs, concentrated in 3 files…" | no tool_use → loop ends, display the answer |
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 → 显示答案
Notice history grows every turn: turn 3's request contains the entire conversation of the first two turns. That is why long conversations get slower and more expensive, and why chapter 12's "compaction" exists.
6.4 Streaming receive: why the answer is "typed" one character at a time
Generating a reply takes time; Claude Code uses streaming transport: the server writes a bit and sends a bit. The request-issuing code (near claude.ts:1822) has a telling comment:
// 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 急刹车的"刹车线"直接通到这里
)
Line by line: the software deliberately does not use the SDK's high-level stream object, and uses the raw stream. Why? A tool call's arguments are JSON, and in streaming they arrive as tiny fragments (input_json_delta events); the SDK tries to parse "JSON that is not yet complete" on every fragment; the longer the content, the more repeated parsing, an O(n²) waste. Claude Code concatenates the fragments into a string and parses once when complete, skipping all the repeated work. When you once ask the model to write a large file, the gap from this optimization is very visible.
The streaming format is called SSE (Server-Sent Events). After the software receives it, it enters a large switch (claude.ts:1980-2303) to sort:
flowchart LR
SSE["服务器 SSE 事件流"] --> SW{"事件类型 switch"}
SW -->|"content_block_start
(:1995)"| A["初始化一个内容块
(文字块或 tool_use 块)"]
SW -->|"input_json_delta
(:2087)"| B["把工具入参的 JSON 碎片
逐段拼接到字符串上"]
SW -->|"content_block_stop
(:2171)"| C["块收齐了 → 组装 yield
JSON 只在此刻解析一次"]
SW -->|"message_delta
(:2213)"| D["回填 usage 统计和
stop_reason(模型为何停笔)"]
Matching life-support: a stream idle watchdog of 90 seconds (if the server is silent for that long, treat it as a broken stream, error and retry); model fallback — when the current model is overloaded, automatically switch to a backup and retry.
6.5 A clever optimization: start work while still receiving
The model's words are not finished, and the tool is already running. That is StreamingToolExecutor (src/services/tools/StreamingToolExecutor.ts): the moment a tool call's arguments are complete, start executing without waiting for the rest of the model's explanation. For example, the first half of the reply is "I want to run npm test"; while the explanatory second half is still in flight, npm test is already running on your machine.
Execution discipline is decided by the tool's isConcurrencySafe property (StreamingToolExecutor.ts:105-133):
- read-only tools may run in parallel (reading files and searching do not interfere), concurrency cap 10 (env
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCYis tunable); - writes are strictly queued: if even one unsafe tool is in the same batch, everyone goes one at a time, so they do not step on each other;
- a Bash error cancels sibling commands (
siblingAbortController,:48-59): ifmkdir fooin a batch of commands fails, the later "write a file into foo" is cancelled immediately — because commands often have a dependency chain, and if the front collapsed the back is pointless.
6.6 A key invariant: every "letter" must have a "reply"
This is the easiest-to-miss, and hardest, rule in the whole system: the API requires that every tool_use block be paired with a tool_result, otherwise the next request is refused outright (HTTP 400).
But in reality the model may have issued a tool-call request and then been interrupted by ESC, by a model switch, by an exception — what about those "orphan tool_use"s? The answer is yieldMissingToolResultBlocks (query.ts:123-149): on interrupt / fallback / exception, the software automatically patches a fake tool_result for every orphan (content is an error gloss "interrupted," tagged is_error: true). Like a letter that must have a reply, even if the reply is "the other party does not want to talk." Without this patch, any interrupt would permanently wreck the conversation.
6.7 The loop's emergency exits: the full set of terminal reasons
Besides the normal exit completed (the model no longer calls tools), the loop has a whole set of emergency exits (collectively Terminal states in the source):
| Terminal reason | When it happens |
|---|---|
✅ completed |
the model finished normally, no tool call |
🛑 aborted_streaming / aborted_tools |
you pressed ESC during stream receive / tool execution |
📏 prompt_too_long |
every compaction means failed; the window truly cannot hold it |
🔁 max_turns |
hit the caller-set turn cap (e.g. the SDK's maxTurns) |
💥 model_error / image_error |
the API keeps erroring / image processing failed |
🪝 stop_hook_prevented / hook_stopped |
your configured Stop hook said "stop" |
⛔ blocking_limit |
compaction is disabled and we hit the window warning line, leaving room for you to /compact by hand |
Two "life-extension" mechanisms are also worth a mention: output-truncation recovery — when the model's reply is truncated by max_output_tokens, the software has it "keep writing," recovering at most 3 times (MAX_OUTPUT_TOKENS_RECOVERY_LIMIT); token-budget nudge — when money (the token budget) is almost gone, inject a meta message into the conversation urging the model to "wrap up."
🔍 Source pointers:
src/query.ts:219,241,265-284,307,659,829-834,1357,1714-1716(loop skeleton) |src/query.ts:123-149(patch orphantool_result) |src/services/api/claude.ts:1822,1980-2303(raw stream and SSE switch) |src/services/tools/StreamingToolExecutor.ts:48-59,105-133(concurrency discipline and sibling cancel)
7. The model's "hands": the tool system and the permission gate
7.1 What a tool is: an object of about 40 fields
Every capability the model can use is a "Tool" object (defined in src/Tool.ts), about 40 built-in tools. Each tool is made of five groups of fields:
| Group | Fields | What they do |
|---|---|---|
| Identity | name / aliases |
the model calls by name |
| Schema | inputSchema (Zod) |
parameter format; zodToJsonSchema turns it into JSON Schema for the API |
| Behavior | call / validateInput / isReadOnly / isConcurrencySafe |
the function that actually works, plus concurrency properties |
| Permission | checkPermissions |
the tool itself judges "is this particular operation dangerous" |
| UI | renderToolUseMessage etc. |
how it is shown in the terminal (Bash shows $ command, Edit shows a diff) |
Every tool is minted uniformly by the buildTool factory (Tool.ts:783). The factory fills in fail-closed defaults — any field you did not define explicitly defaults to the most conservative behavior (prefer one extra ask over one wrong allow). The single source of truth for all tools is getAllBaseTools() (tools.ts:193); about 40 built-ins are registered there; then getTools() filters by current mode and strips by deny rules; assembleToolPool() merges MCP tools, dedupes, and sorts — stable sort matters, because the tool list is sent to the API every turn, and a stable order is what hits the prompt cache.
How does a tool's "spec sheet" get into the model's hands? toolToAPISchema (utils/api.ts:119) turns each tool into {name, description: await tool.prompt(), input_schema}, sent as the tools parameter of every turn (claude.ts:1235). When there are too many tools there is also a ToolSearch mechanism (defer_loading): first give the model only a "tool-search tool"; it looks up the tools it needs by keyword and then loads the full schema — a bookshelf that first only shows titles, and you pull the book you need.
7.2 Permission adjudication: a five-gate lock
This is the core of the system's safety. The model has no way to execute a command directly — it can only "file a request"; approval sits with the software (and you). The adjudication function is hasPermissionsToUseToolInner (src/utils/permissions/permissions.ts:1158); the gate order is as follows (source comments labeled as written):
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:弹窗问你
}
flowchart TD
A["模型申请:Bash('某命令')"] --> B{"① deny 规则?"
如你把 rm 加进黑名单}
B -->|命中| DENY["❌ 拒绝,原因回喂模型"]
B -->|否| C{"② ask 规则?"
逢用必问的清单}
C -->|命中| ASK["❓ 弹窗问你"]
C -->|否| D{"③ 工具自查 checkPermissions
Bash:解析命令 AST,匹配前缀规则
如 Bash(git *),比对危险模式清单"}
D -->|判定危险| DENY
D -->|判定安全| ALLOW["✅ 直接执行"]
D -->|拿不准| E{"④ 权限模式
plan 模式全拦截 / acceptEdits 放行改文件
bypassPermissions 全放行 / auto 交给分类器"}
E -->|模式说放行| ALLOW
E -->|模式说拦| F{"⑤ alwaysAllow 规则?
你以前点过'始终允许 npm test'?"}
F -->|命中| ALLOW
F -->|否| ASK
ASK -->|允许一次| ALLOW
ASK -->|始终允许| WRITE["✅ 执行 + 规则写入 settings
下次不再问"]
ASK -->|拒绝| DENY
Bash's "self-check" is worth unpacking: it parses the command into an AST (syntax tree) and does two matches — prefix rules (you configured Bash(git *) as allow, so git status and git log are let through); dangerous-pattern list (src/utils/permissions/dangerousPatterns.ts maintains a list of "interpreters that can run arbitrary code" such as python, node, npx, bash; allow rules with those prefixes get extra scrutiny — because Bash(python *) equals opening everything). In some scenes the software also lets a small model "pre-score" the command's danger (startSpeculativeClassifierCheck speculative classifier); high-confidence-safe commands are let through, reducing dialog interruption.
7.3 Four commands, four fates
Drop the adjudication onto four concrete examples:
| Command | Fate | Gates it walked |
|---|---|---|
git status |
✅ let through | you previously clicked "always allow git *" → hit gate ⑤ alwaysAllow; or the tool's self-check judged it read-only-safe |
rm -rf / |
❌ blocked outright | hit a dangerous pattern / deny rule; gate ① or ③ vetoes, no dialog at all — some commands should not even be asked |
npm test |
❓ dialog asks you | none of the five gates hit → fallback ask. "Allow once" runs only this time; "always allow" writes the rule into config, and later npm test stops bothering you |
| After the model sees "denied" | 🔁 self-corrects | the denial reason is fed back as a tool result; the model usually tries again in a different posture (e.g. finish the task with a read-only method) |
The fourth case reveals a design philosophy: a model mistake does not crash the system. The model invents a tool name, the software replies No such tool and lets it self-correct; arguments are wrong, Zod validation bounces them; a command is denied, the reason is fed back. Every failure is translated into "textual feedback" the model can read; the loop continues — the program does not crash.
7.4 The execution chain in full: from "the model wants to" to "it actually runs"
flowchart TD
A["模型说:tool_use Bash('rm -rf node_modules')"] --> B["1. findToolByName 找工具
找不到 → 回 'No such tool' 让模型自我纠正"]
B --> C["2. inputSchema.safeParse 校验参数
command 是字符串吗?"]
C --> D["3. validateInput 工具自检"]
D --> E["4. PreToolUse 钩子
toolHooks.ts:435,用户脚本可拦截/改写"]
E --> F{"5. canUseTool 权限裁决
7.2 节的五道关卡"}
F -->|拒绝| G["拒绝原因回喂模型"]
F -->|放行| H["6. tool.call 真正执行(:1207)
起 shell 进程,收集输出"]
H --> I["7. 结果格式化 mapToolResultToToolResultBlockParam"]
I --> J["8. PostToolUse 钩子
可对结果追加反馈"]
J --> K["9. 包成 tool_result 的 user 消息
追加进对话历史 → 主循环下一圈"]
(Source spine: runToolUse at src/services/tools/toolExecution.ts:337.) A tool execution throwing an exception also does not crash: the exception is wrapped as a tool_result with is_error: true and fed back; the model reads the error and thinks of another way.
7.5 Hooks: a user-installed "automated checkpoint"
A hook is a shell command you register in a config file, triggered at 20 kinds of event (PreToolUse / PostToolUse / UserPromptSubmit / SessionStart / Stop / PreCompact, etc.). The execution mechanism (src/utils/hooks.ts) is child_process.spawn starting a subprocess to run your script: JSON data in via stdin, your script writes JSON back on stdout. The returned JSON can express allow/deny/ask/hookUpdatedInput (rewrite tool input) / additionalContext (append material for the model) / stop (stop the loop).
Example: you can configure a PreToolUse hook that, whenever the model wants to Edit a file, first runs a format check; or a UserPromptSubmit hook that auto-translates every question of yours into English before sending it to the model. The difference between hooks and the permission system: permission is a gate built into the software; a hook is a pipeline you install yourself.
7.6 Skills and MCP: two ways to enlarge the toolbox
- Skills:
src/skills/scans~/.claude/skills, the project's.claude/skills, plugin directories, and built-in directories; eachSKILL.md(with YAML frontmatter) is a skill. It uses progressive disclosure: day to day, only a skill listing is injected into context (getSkillListingAttachmentsinattachments.ts, saving tokens); when the model decides to use a skill, it loads the full spec via the Skill tool. - MCP tools: tools provided by an external MCP server are wrapped by
MCPToolinto a Tool object of the same shape, treated identically to built-ins — the same schema validation, the same permission adjudication, the same execution chain. To the model and the permission system, there is no way to tell whether a tool is built-in or foreign, which is the point of the design.
🔍 Source pointers:
src/Tool.ts:783(buildTool) |src/tools.ts:193(getAllBaseTools) |src/utils/api.ts:119(toolToAPISchema) |src/utils/permissions/permissions.ts:1158(adjudication order) |src/utils/permissions/dangerousPatterns.ts(dangerous patterns) |src/services/tools/toolExecution.ts:337,1207(execution chain) |src/services/tools/toolHooks.ts:435(PreToolUse) |src/utils/hooks.ts(spawn to run hooks)
This chapter is a "quick look" at the tool system: what a tool is, how permission governs. As for the look, parameters, and quirks of about 40 built-in tools — the complete field guide, one by one, is chapter 8; the full ecosystem of skills and MCP is chapter 10.
8. A field guide to the tools: the model's 40 weapons, one by one
Chapter 7 covered a tool object's field structure and the permission gate — that was the "institutional" quick look. This chapter dumps the whole toolbox on the table and looks at each piece: what each tool is named, what job it does, what parameters it eats, what quirks it has. Under src/tools/ there are 39 tool directories (plus testing/ and shared/), plus the MCPTool family that wraps MCP tools, totaling 40-odd "weapons," not one skipped.
First three pieces of infrastructure every tool shares, then "field-guide cards" in 11 groups.
8.1 How the tool pool is assembled: register → filter → sort
The tool list in the model's hands is not a hard-coded table; it is assembled on the spot before every request, in three stages:
flowchart LR
A["① getAllBaseTools() 注册
tools.ts:193-251
唯一事实来源:约 40 个内置工具在此登记
核心约 14 个无条件启用
其余靠 feature()/环境变量门控"]
A --> B["② getTools() 二次过滤
tools.ts:271-327
极简模式只留 Bash/Read/Edit
deny 规则整体剔除(模型根本看不到)
REPL 模式隐藏原语工具"]
B --> C["③ assembleToolPool() 合并排序
tools.ts:345-367
内置与 MCP 工具合并去重(内置优先)
各自按名排序,内置必须排成连续前缀"]
C --> D["📮 作为 tools 参数发给 API"]
Each of the three stages has one point worth remembering:
- Single source of truth: to know "which tools this version actually has," look only at the one function
getAllBaseTools()— it carries a header comment warning that changes must stay in sync with the prompt-cache config (chapter 5: the tool list is part of the static prefix; messing with it scrambles the cache). - Deny rules strip before the model sees the list (
filterToolsByDenyRulesattools.ts:262-269): if you blacklist a tool, it is not "requested then denied"; it does not appear in the spec sheet at all — the model does not even know it exists, cut off at the source. - Built-ins must form a contiguous prefix: MCP tools are appended after built-ins, not mixed by name. Why? Cache matches by prefix; if a foreign MCP tool is inserted into the middle of built-ins, every downstream cache key is scrambled, which is throwing money away every turn.
8.2 The three-flag system: every tool's "safety tags"
Every tool ships with three boolean flags (defaults at Tool.ts:750-761 — all default false; undeclared means most conservative):
| Flag | Question it answers | Who reads it |
|---|---|---|
isReadOnly |
does this operation only look, not change? | the permission system (read-only is easier to allow), UI display |
isConcurrencySafe |
can it run at the same time as other tools? | the streaming executor (section 6.5: read-only parallel, writes serial, cap 10) |
isDestructive |
is this operation destructive? | permission warnings (the dialog adds a warning) |
The clever part is that these three flags can be functions, judged dynamically from input. Two typical cases:
- Bash's
isReadOnly(input)(BashTool.tsx:434-441): not a one-cut "Bash is always dangerous," but parse the command itself —ls,git statuscount as read-only and parallelizable,rmdoes not; - ExitWorktree's
isDestructive(input)(:168-170):action: "keep"keeping the workspace is not destructive,action: "remove"deleting it is — the same knife, chopping vegetables versus chopping a person, treated differently.
8.3 When output is too large: the persist-to-disk mechanism
A tool result has to go back into conversation history, but one command's output may be hundreds of KB. Each tool has a maxResultSizeChars cap: Bash 30000 characters, Grep 20000 (one of the tightest caps in the whole toolbox, forcing the model to write a precise search), Read is Infinity (truncation is done in-house), most tools 100000. What happens after the cap?
flowchart TD
A["工具执行完毕,结果超大"] --> B{"超过 maxResultSizeChars?"}
B -->|没超| C["直接作为 tool_result 回传"]
B -->|超了| D["完整输出写入临时文件落盘"]
D --> E["tool_result 只带:开头摘要 + 文件路径
persistedOutputPath,如 BashTool.tsx:424,292"]
E --> F["模型需要细节时
用 Read/Grep 去落盘文件里自取"]
The idea is "do not show the model the whole picture, but leave it an index": history does not stuff 5MB of logs, only a sentence "the full output is at this path; read whichever slice you need." This and chapter 12's compaction-defense applyToolResultBudget are two applications of the same philosophy.
8.4 How to read a field-guide card
Every card below has a uniform format:
Name — one-sentence job Key parameters · call example (JSON) · details (the 1–2 most characteristic implementation / safety designs, with line numbers)
Legend: 👀 read-only | ⚡ concurrency-safe | 🐢 delayed-loaded by ToolSearch (section 8.15).
8.5 File operations (4): the iron law of read-before-write
Read — the model's eyes: read a local file. 👀⚡
- Parameters: file_path (absolute path, required), offset (start line), limit (line count).
- Example: before fixing a bug {"file_path": "/repo/src/index.ts", "offset": 100, "limit": 80}.
- Details: dual output caps — a file over 256KB is refused outright (MAX_OUTPUT_SIZE at utils/file.ts:48); content over 25000 tokens errors (limits.ts:18, overridable by CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS). Comments at limits.ts:9-13 record an A/B rollback: after overshoot, "silent truncate" lost to "error" — because silent truncate lets the model think it finished the whole file and draw conclusions from incomplete information.
Edit — the workhorse for changing code: exact string replace.
- Parameters (types.ts:6-19): file_path, old_string, new_string, replace_all (default false).
- Example: {"file_path": "/repo/app.ts", "old_string": "const x = 1", "new_string": "const x = 2"}.
- Details: a file that has not been Read errors outright "File has not been read yet" (FileEditTool.ts:281); detecting an external change (you edited by hand, a linter edited) requires a fresh Read; .ipynb is refused and steered to NotebookEdit (:270) — each tool keeps its own jurisdiction.
Write — write / overwrite a whole file.
- Parameters: file_path, content.
- Example: {"file_path": "/repo/new.ts", "content": "export const …"}.
- Details: overwriting an existing file likewise forces "Read first," and detects "the file was changed since I last read it" (FileWriteTool.ts:203,216), so a blind overwrite does not wipe the content you just edited by hand.
NotebookEdit — edit a Jupyter notebook cell. 🐢
- Parameters: notebook_path, cell_id, new_source, cell_type (code/markdown), edit_mode (replace/insert/delete).
- Example: {"notebook_path": "/repo/a.ipynb", "cell_id": "c3", "new_source": "print(1)", "edit_mode": "replace"}.
- Details: shouldDefer: true (:94) — day to day the spec sheet is not even sent to the model; loaded when used (section 8.15).
Why is "read before write" an iron law? The model's picture of a file may be stale — you just changed two lines in the editor, it does not know. Forcing Read equals "refresh cognition" before write:
flowchart TD
A["模型想 Edit/Write 一个文件"] --> B{"本会话 Read 过这个文件吗?"}
B -->|没读过| C["❌ 报错 File has not been read yet
FileEditTool.ts:281
模型自我纠正:先 Read"]
B -->|读过| D{"上次 Read 之后被外部改过吗?
你手动改的 / linter 改的"}
D -->|改过| E["❌ 要求重新 Read
FileWriteTool.ts:203,216"]
D -->|没改| F["✅ 执行写入"]
8.6 Search (2)
Glob — find files by filename pattern, sorted by mtime. 👀⚡
- Parameters: pattern (e.g. **/*.ts), path (search root, default current directory).
- Example: find all test files → {"pattern": "**/*.test.ts"}.
- Details: the ant internal build, because it inlines substitutes like bfs/ugrep, removes Glob/Grep from the tool list entirely (tools.ts:198-201) — the toolbox also "diets" by edition.
Grep — content regex search via ripgrep. 👀⚡
- Parameters: pattern, path, glob, output_mode (content/files_with_matches/count), -A/-B/-C (context lines), -n, -i, type, head_limit (default 250), offset, multiline.
- Example: {"pattern": "fetchUser", "output_mode": "content", "-n": true}.
- Details: maxResultSizeChars: 20_000 (GrepTool.ts:164) — as said above, one of the tightest caps in the whole toolbox. Design intent: overly broad search results are a common disease of models; a hard cap forces the regex to be written accurately.
8.7 Command execution (3)
Bash — the model's two hands: run a shell command.
- Parameters (:227-247): command, timeout, description (optional, z.string().optional() — a plain-language description for humans), run_in_background, dangerouslyDisableSandbox.
- Example: {"command": "npm test", "description": "Run test suite"}.
- Detail 1: compound commands are split then matched. When the model requests ls && git push, preparePermissionMatcher splits it into subcommands and runs each through permission rules (BashTool.tsx:447-465) — you having allowed Bash(ls *) does not mean git push can hitch a ride; see the figure below.
- Detail 2: internal fields hidden from the model. _simulatedSedEdit is an internal marker "after a sed preview is approved," deliberately omitted from the model-visible schema (:249-259 comments) — so the model cannot use it to bypass the permission sandbox and write arbitrary files. Output over 30000 characters takes the persist-to-disk of section 8.3 (:424,292).
flowchart TD
A["模型申请:Bash('ls && git push')"] --> B["preparePermissionMatcher
把复合命令拆成子命令
BashTool.tsx:447-465"]
B --> C["子命令 1:ls"]
B --> D["子命令 2:git push"]
C --> E{"匹配 Bash(ls *)?
✅ 已允许"}
D --> F{"匹配 Bash(git push *)?
❓ 没有规则"}
E --> G["任何一条过不了,整条命令都要弹窗
权限按最严的子命令算"]
F --> G
PowerShell — the PowerShell edition of Bash on Windows.
- Parameters almost identical to Bash (:228-233).
- Enablement: getPlatform()==='windows', and ant on by default / external needs CLAUDE_CODE_USE_POWERSHELL_TOOL (utils/shell/shellToolUtils.ts:17-22; tools.ts:242). The directory ships its own safety checks such as gitSafety.ts, destructiveCommandWarning.ts.
REPL — an ant-internal "batch-operate a VM" mode switch.
- Enablement: USER_TYPE==='ant' and the CLI entry; CLAUDE_CODE_REPL=0 can turn it off (REPLTool/constants.ts:23-30); SDK does not enable it by default.
- Details: when on, the 8 primitive tools Read/Write/Edit/Glob/Grep/Bash/NotebookEdit/Agent are hidden from the model (REPL_ONLY_TOOLS :37-46); the model must call them through REPL inside the VM — equivalent to swapping "act directly" for "act in a sandbox."
8.8 Network (2)
WebFetch — fetch a URL's content, then run a small prompt so the model processes that content and returns it. 👀🐢
- Parameters: url, prompt (what to do with the fetched content).
- Example: {"url": "https://docs.example.com/install", "prompt": "提取安装步骤"}.
- Details: preapproved.ts:14 maintains a "code-related domain allowlist," only allowing WebFetch GETs to skip review; the file header has a SECURITY WARNING (:5-11): this allowlist must not be reused by other tools — the allowlist was set against a "read-only webpage" threat model; another tool borrowing it would be out of scope.
WebSearch — web search. 🐢
- Parameters: query (≥2 characters), allowed_domains, blocked_domains.
- Example: {"query": "vite 7 migration guide"}.
- Details: isEnabled() is decided by API provider — firstParty is on directly; Vertex requires a Claude 4.0+ model. Feature availability depends on "how you pay" (chapter 11 covers the auth system).
8.9 Sub-agents and task management (8)
Agent — spawn a sub-agent to finish a task independently (isolated context; can run in parallel or in the background).
- Parameters (:82-126): description (3–5 words), prompt (the brief), subagent_type, model (sonnet/opus/haiku), run_in_background, name/team_name, mode, isolation: worktree, cwd.
- Example: three {"description": "调研方案A", "prompt": "…", "run_in_background": true} exploring three routes in parallel.
- Details: isReadOnly() returns true — the Agent tool itself does not write disk; permission checks are all delegated to tools inside the sub-agent (:1264-1266); output cap 100000 characters (:229). Chapter 13 covers the two run modes of a sub-agent.
TaskOutput — read a background task's output; can block and wait. 👀
- Parameters: task_id, block (default true), timeout (≤600000ms, default 30000).
- Example: {"task_id": "a1b2c3", "block": true, "timeout": 30000}.
- Details: isEnabled() is hard-coded "external" !== 'ant' (near :163) — the ant internal build actually disables this tool, another example of edition differences.
TaskStop — terminate a background task. 🐢
- Parameters: task_id (compatible with the old name shell_id).
- Example: {"task_id": "b7"}.
TaskCreate / TaskGet / TaskUpdate / TaskList (the TodoV2 quartet) — structured task-list create/get/update/list. 🐢
- Traits: support blocking relations addBlocks/addBlockedBy, owner, metadata; TaskUpdate's status accepts 'deleted'.
- Enablement: isTodoV2Enabled() (tools.ts:218-220) — forced on in non-interactive sessions; CLAUDE_CODE_ENABLE_TASKS forces it on (utils/tasks.ts:133-139).
TodoWrite — classic todo-list write (content/status/activeForm).
- Details: mutually exclusive with the TaskCreate family — isEnabled() { return !isTodoV2Enabled() } (near :51). The old and new todo systems are one-or-the-other; they never appear in the toolbox together:
flowchart LR
Q{"isTodoV2Enabled()?
utils/tasks.ts:133-139"}
Q -->|"是(新)"| A["✅ TaskCreate/Get/Update/List 上岗
❌ TodoWrite 隐身"]
Q -->|"否(旧)"| B["✅ TodoWrite 上岗
❌ Task 四件套不注册"]
8.10 Multi-agent collaboration (3)
SendMessage — send a message to a teammate agent.
- Parameters: to (teammate name, * broadcast; under UDS_INBOX also uds:<socket>, bridge:<session> cross-process delivery), summary (5–10 words UI preview), message.
- Example: {"to": "researcher", "summary": "询问认证方案结论", "message": "你查的 OAuth 方案有结论了吗?"}.
- Details: isReadOnly(input) is dynamic — a pure-text message counts as read-only; isEnabled() = isAgentSwarmsEnabled().
TeamCreate / TeamDelete — create / dissolve an agent team.
- TeamCreate parameters: team_name, description, agent_type.
- Enablement: isAgentSwarmsEnabled() — always on for ant; external needs CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS or --agent-teams (utils/agentSwarmsEnabled.ts:24-32; tools.ts:228-230 uses lazy require to break a cyclic dependency).
8.11 User interaction and configuration (4)
AskUserQuestion — pop 1–4 multiple-choice questions at you (with option annotations) and take a structured answer. 👀⚡
- Parameters: questions (array of 1–4, with uniqueness-check refine).
- Example: {"questions": [{"question": "选哪种数据库?", "options": [{"label": "Postgres"}, {"label": "SQLite"}]}]}.
- Details: when --channels (Telegram/Discord remote channels) is active, isEnabled() returns false (:135-145) — nobody is sitting at the terminal, so a dialog would hang the whole session. Feature availability depends on "is there a person in front of you."
Brief (API name SendUserMessage) — in KAIROS proactive-assistant mode, send you a rich-text message, optionally with attachments; status: proactive for a proactive push.
- Details: the directory is named Brief but the API name is SendUserMessage (prompt.ts:1); isEnabled() requires both feature('KAIROS') and a runtime entitlement (:122-135).
Skill — invoke an installed skill by name (inject its SKILL.md instructions).
- Parameters: skill, args.
- Example: {"skill": "commit"}.
- Details: this is the stage-two switch of "progressive disclosure"; chapter 10 covers it.
Config — read/change a setting.
- Parameters: setting, value (omit = read).
- Details: isReadOnly(input) is dynamic — a read with no value counts as read-only (:90-92); registered only when USER_TYPE==='ant' (tools.ts:214).
8.12 Mode switching (4)
EnterPlanMode — enter "plan only, do not act." No parameters (EnterPlanModeTool.ts:21-26).
ExitPlanMode — submit a plan for your approval.
- Parameters: allowedPrompts — declare the permission categories needed to implement the plan (describes action categories, not concrete commands, e.g. "run tests").
- Details: when channels are active, the enter and exit tools are disabled together (:167-178) — to avoid "you can enter but cannot leave."
EnterWorktree — create a temporary git worktree as an isolated workspace.
- Parameters: name (slug-validated, optional).
ExitWorktree — leave a worktree.
- Parameters: action: keep | remove; uncommitted changes require an explicit discard_changes: true.
- Details: isDestructive is decided from the input (section 8.2); a session-scoped scope guard only recognizes worktrees created by this session (:177-182) — this tool cannot be used to touch another session's workspace. Enablement: isWorktreeModeEnabled() is currently always true (tools.ts:225).
8.13 Scheduling and triggers (5)
Sleep — wait a specified duration.
- Details: does not occupy a shell process; can be interrupted; its prompt recommends the model prefer it over Bash(sleep), and reminds "each wake-up is one API call; the prompt cache expires in 5 minutes" (SleepTool/prompt.ts:3) — even sleeping has to count the cache bill. Enablement: feature('PROACTIVE') || feature('KAIROS') (tools.ts:25-28).
CronCreate / CronDelete / CronList — register a cron timed task that enqueues a prompt when due. 🐢
- CronCreate parameters: cron (5-field local time), prompt, recurring (false = one-shot), durable (true persists to .claude/scheduled_tasks.json, surviving restarts).
- Example: {"cron": "*/30 * * * *", "prompt": "检查 CI 状态", "recurring": true, "durable": true}.
- Enablement: feature('AGENT_TRIGGERS') (tools.ts:29-35).
RemoteTrigger — manage remote triggers.
- Parameters: action: list | get | create | update | run + trigger_id + body.
- Enablement: feature('AGENT_TRIGGERS_REMOTE') (tools.ts:36-38).
8.14 LSP and internal/test tools (3)
LSP — call a language server for semantic code navigation.
- Parameters: operation (goToDefinition/findReferences/hover/documentSymbol/workspaceSymbol/goToImplementation/prepareCallHierarchy/incomingCalls/outgoingCalls), filePath, line, character (1-based).
- Example: {"operation": "findReferences", "filePath": "/repo/src/api.ts", "line": 42, "character": 10}.
- Enablement: ENABLE_LSP_TOOL env (tools.ts:224). Grep is "text-level" search; LSP is "semantic-level" search — it knows who is a real function reference and who is only a same-named string.
StructuredOutput — in non-interactive (SDK/headless) mode, have the model emit a structured result matching a caller-supplied JSON Schema, Ajv-validated (SyntheticOutputTool/:20).
- Enablement: non-interactive sessions only (:24-28) — when a program calls Claude, what it wants is an answer it can JSON.parse directly, not prose.
TestingPermissionTool — a test-only mock of the permission dialog, registered only when NODE_ENV==='test' (tools.ts:244) — automated tests have nobody to click a dialog, so this pretends to be the user.
Additionally, tools.ts cites a batch of tools whose files are missing from this snapshot (feature-gated experiments / internal): TungstenTool, SuggestBackgroundPRTool, MonitorTool, SendUserFileTool, PushNotificationTool, SubscribePRTool, VerifyPlanExecutionTool (CLAUDE_CODE_VERIFY_PLAN=true), OverflowTestTool, CtxInspectTool, TerminalCaptureTool, WebBrowserTool, SnipTool, ListPeersTool, WorkflowTool — knowing they exist is enough; the field guide inventories what the snapshot actually has.
8.16 MCPTool: the "naturalization" wrapper for foreign tools
tools/MCPTool/MCPTool.ts is only a placeholder template (name: 'mcp', methods annotated "Overridden in mcpClient.ts"). Real wrapping happens in fetchToolsForClient at services/mcp/client.ts:1743-1832:
- send
tools/listto the MCP server and get the tool list; - each tool is expanded/overridden as
{...MCPTool, name: fullyQualifiedName, ...}(:1769-1773), cloning a "local edition"; - naming rule:
mcp__<normalized-server-name>__<tool-name>(mcpStringUtils.ts:48), e.g.mcp__github__create_issue. An SDK server can setCLAUDE_AGENT_SDK_MCP_NO_PREFIXto skip the prefix (:1760-1763); - safety metadata comes from MCP annotations: the server's self-reported annotations are mapped onto the three flags of section 8.2 (
:1795-1809);mcpInfolets permission rules match a whole server; - overlong descriptions are truncated at
MAX_MCP_DESCRIPTION_LENGTH(:1789-1794); permissions default to passthrough, with a suggestion that the user add alocalSettingsallow rule.
flowchart LR
subgraph SR["MCP 服务器自报家门(annotations)"]
A["readOnlyHint"]
B["destructiveHint"]
C["openWorldHint"]
end
A --> D["isReadOnly +
isConcurrencySafe"]
B --> E["isDestructive"]
C --> F["isOpenWorld"]
D --> G["包装好的 MCPTool
与内置工具一视同仁:
同样 schema 校验、同样五关权限裁决"]
E --> G
F --> G
Three accompanying "sidekick" tools:
McpAuthTool(client.ts:63): when a server needs OAuth, dynamically register anmcp__<server>__authenticatetool to walk you through authorization (client.ts:2318,2331);ListMcpResourcesTool: list MCP resources (parameterserver?);ReadMcpResourceTool: read an MCP resource (parametersserver+uri).
The last two are registered at the end of getAllBaseTools (tools.ts:245-246) but added on demand by getTools() as specialTools (:301-307).
🔍 Source pointers:
src/tools.ts:193-251,262-269,271-327,345-367(register/filter/sort) |src/Tool.ts:750-761(three-flag defaults) |src/tools/BashTool/BashTool.tsx:227-259,424,434-465(Bash parameters / hidden fields / compound-command split) |src/tools/FileEditTool/FileEditTool.ts:281(read before write) |src/tools/ToolSearchTool/prompt.ts:62-108,ToolSearchTool.ts:132-216(defer list and search scoring) |src/services/mcp/client.ts:1743-1832(MCPTool wrapping)
9. A field guide to slash commands: how 60+ shortcuts work
Chapter 4 said: input starting with / is routed to the command system. This chapter spreads the command system out: first recap the three command types, then a master table of 60+ commands, then pick 8 of the most representative and take them down to source, and finally teach you to write your own.
9.1 Recap: three command types, three fates (plus two mechanisms)
A command object has three types (types/command.ts), with sharply different fates:
flowchart TD
A["你敲下 /xxx 回车"] --> B["parseSlashCommand 解析"]
B --> C["findCommand 精确匹配
commands.ts:688:名字 / 全名 / 别名
查不到 → Unknown command,绝不瞎猜"]
C --> D{"什么类型?"}
D -->|"local-jsx 🖥️"| E["渲染终端 UI(设置面板、选择器)
不发 AI"]
D -->|"local ⚙️"| F["执行本地函数(/clear、/cost)
不发 AI"]
D -->|"prompt 📝"| G["展开成一段预制提示词
发给 AI"]
Two easy-to-miss mechanisms:
- Lazy load: a command's
index.tsstores only metadata (name, description, type); the body isload: () => import(...)dynamically loaded — e.g./insightsis 115KB of implementation; if you never use it, it never occupies memory. Same philosophy as chapter 3's--versionfast path: do less at startup. - Registration order: the
COMMANDS()array (commands.ts:258) exposes about 60 commands; additionallyINTERNAL_ONLY_COMMANDS(:225) is internal-only.loadAllCommands()(:449) merges in the order bundled skills → built-in plugin skills → directory skills → workflows → plugin commands → plugin skills → hard-coded built-in commands;findCommandtakes the first match in array order — so a same-named user/plugin skill ranks ahead of a hard-coded built-in command, and you can "override" a built-in.
9.2 Master table: 60+ commands at a glance
Type icons: ⚙️ local (local function) | 🖥️ local-jsx (terminal UI) | 📝 prompt (send to the model). Marked 🚪 are feature-gated / internal-only.
① Session management
| Command | Type | One-sentence description |
|---|---|---|
| /clear (aliases reset/new) | ⚙️ | Clear conversation history and start a new session — in substance a "soft restart" (section 9.3) |
| /compact | ⚙️ | Clear history but keep a summary; can take an argument as a custom summary instruction |
| /resume (alias continue) | 🖥️ | Resume a previous conversation: pop a picker or search by ID/title |
| /branch (alias fork) | 🖥️ | Fork a conversation branch at the current node |
| /rewind (alias checkpoint) | ⚙️ | Roll code and/or conversation back to a previous checkpoint |
| /rename | 🖥️ | Rename the current conversation; with no argument, auto-generate a name (immediate) |
| /export | 🖥️ | Export the conversation as .txt or copy to the clipboard |
| /copy | 🖥️ | Copy the last reply; /copy N takes the Nth from the end |
| /exit (alias quit) | 🖥️ | Leave the REPL (immediate) |
| /session (alias remote) | 🖥️ | Show the remote-session URL and QR code |
| /btw | 🖥️ | Ask a side-quest question quickly, without interrupting the main conversation (immediate) |
| /plan | 🖥️ | Enter plan mode or view the current plan |
| /share /summary /teleport /onboarding /backfill-sessions | 🚪 | Five internal stubs; empty shells in the external build |
② Model and configuration
| Command | Type | One-sentence description |
|---|---|---|
| /model | 🖥️ | Set the main-loop model; no argument pops a picker, with an argument switches directly |
| /effort | 🖥️ | Set the model's effort grade low/medium/high/max/auto |
| /fast | 🖥️ | Toggle fast mode |
| /advisor | ⚙️ | Set/unset the advisor model |
| /config (alias settings) | 🖥️ | Open the settings panel |
| /output-style | 🖥️ | Deprecated; tells you to use /config |
| /statusline | 📝 | Dispatch a statusline-setup sub-agent to configure the status bar |
| /theme | 🖥️ | Change the terminal theme |
| /color | 🖥️ | Set this session's prompt-bar color (immediate) |
| /vim | ⚙️ | Toggle Vim/Normal editing mode |
| /keybindings | ⚙️ | Open or create the keybinding config file |
| /terminal-setup | 🖥️ | Install the Shift+Enter newline keybinding |
| /voice | ⚙️🚪 | Toggle voice mode (feature-gated) |
| /brief | 🖥️🚪 | Toggle ultra-brief reply mode (KAIROS, immediate) |
③ Context and memory
| Command | Type | One-sentence description |
|---|---|---|
| /memory | 🖥️ | Pick a memory file (the CLAUDE.md family) and open it in an editor |
| /init | 📝 | Scan the codebase, generate/improve CLAUDE.md (the new version can also generate skills and hooks) |
| /context | 🖥️ | A color grid visualizing current context occupancy |
| /add-dir | 🖥️ | Add a new working directory |
| /insights | 📝 | Analyze all sessions and generate an HTML insights report (115KB, the typical lazy-load) |
| /files | ⚙️🚪 | List every file in context (internal) |
④ Tasks and code
| Command | Type | One-sentence description |
|---|---|---|
| /review | 📝 | Review a PR (locally runs gh pr diff and has the model write a review) |
| /ultrareview | 🖥️ | Run a 10–20 minute deep bug hunt on Claude Code on the web |
| /security-review | 📝 | Security-review uncommitted changes |
| /commit | 📝🚪 | Analyze changes and create a git commit (internal-only in this snapshot) |
| /commit-push-pr | 📝🚪 | Commit, push, and open a PR in one shot (internal) |
| /pr-comments | 📝 | Pull PR comments (a transitional shell already moved to a plugin) |
| /diff | 🖥️ | View uncommitted changes and per-turn diffs |
| /tasks (alias bashes) | 🖥️ | List and manage background tasks |
| /agents | 🖥️ | Manage sub-agent config |
| /skills | 🖥️ | List available skills |
| /hooks | 🖥️ | View hook config (immediate) |
| /mcp | 🖥️ | Manage MCP servers (immediate) |
| /plugin (aliases plugins/marketplace) | 🖥️ | Manage plugins and the marketplace (10+ sub-UIs, immediate) |
| /reload-plugins | ⚙️ | Apply pending plugin changes |
| /ultraplan | 🖥️🚪 | Draft a 10–30 minute advanced plan in the cloud (gated) |
| /ide | 🖥️ | Manage IDE integration |
| /sandbox | 🖥️ | Sandbox on/off and config (immediate) |
⑤ Permissions and security
| Command | Type | One-sentence description |
|---|---|---|
| /permissions (alias allowed-tools) | 🖥️ | Manage allow/ask/deny rules (including "retry a denied operation") |
| /privacy-settings | 🖥️ | Privacy settings |
| /doctor | 🖥️ | Diagnose the install and settings |
| /login | 🖥️ | Log in / switch Anthropic account (OAuth2+PKCE) |
| /logout | 🖥️ | Log out |
⑥ Remote and collaboration
| Command | Type | One-sentence description |
|---|---|---|
| /desktop (alias app) | 🖥️ | Continue the session in Claude Desktop |
| /mobile (aliases ios/android) | 🖥️ | Show a QR code to download the mobile App |
| /chrome | 🖥️ | Claude in Chrome settings |
| /install-github-app | 🖥️ | Configure Claude GitHub Actions (full wizard) |
| /install-slack-app | ⚙️ | Install the Slack app |
| /remote-env | 🖥️ | Configure a remote environment |
| /web-setup | 🖥️ | Set up Claude Code on the web |
| /bridge | 🖥️🚪 | Remote Control bridge (gated) |
| /feedback (alias bug) | 🖥️ | Submit product feedback |
⑦ Info and help
| Command | Type | One-sentence description |
|---|---|---|
| /help | 🖥️ | Help and every command (a "mirror" that renders the runtime command registry) |
| /status | 🖥️ | Version, model, account, API connectivity (immediate) |
| /cost | ⚙️ | Session total cost and duration (a purely local ledger) |
| /usage | 🖥️ | Plan usage limits |
| /extra-usage | 🖥️ | Extra-usage config after the quota is exhausted |
| /stats | 🖥️ | Usage stats and activity |
| /release-notes | ⚙️ | Version changelog |
| /upgrade | 🖥️ | Upgrade to the Max plan |
| /passes | 🖥️ | Share a free week with a friend |
| /stickers | ⚙️ | Order stickers (easter egg) |
| /think-back | 🖥️🚪 | Year in review (gated) |
| /heapdump | ⚙️🚪 | Dump the JS heap to the desktop (hidden) |
Three extra pieces of trivia: createMovedToPluginCommand.ts is a compatibility-shell factory for "commands that moved to plugins" (/pr-comments, /security-review use it); REMOTE_SAFE_COMMANDS (commands.ts:619) says remote mode keeps only 17 purely local UI commands; BRIDGE_SAFE_COMMANDS (:651) says phone/web can remotely trigger 6: /compact, /clear, /cost, /summary, /release-notes, /files.
9.3 Deep dives (8 representatives)
/clear — not just "clear the screen"; an 8-step soft restart
Entry clear/clear.ts:4 → clearConversation() at conversation.ts:49:
- First run SessionEnd hooks (1.5s timeout,
:68-74); - Report
tengu_cache_eviction_hint, telling the server to evict the prompt cache (:77-85); - Background agent tasks and main-session tasks (Ctrl+B) survive; foreground tasks are all killed (
:93-107, 135-166); setMessages([])clears messages; updateconversationIdto force a redraw (:109-122);clearSessionCaches()clears session caches (keeps surviving agents' private state), resets cwd, clears file-read cache / discovered skills / nested-memory paths (:124-132);- Reset AppState: file-history snapshots, attribution, MCP clients all reset (triggering reconnect); keep
pluginReconnectKey(:168-191); regenerateSessionId({setCurrentAsParent:true})issues a new session ID (old ID recorded as parent, for lineage analysis); surviving tasks' TaskOutput symlinks point at the new session directory (:203-224);- Re-persist mode/worktree state; run SessionStart hooks to inject messages (
:230-250).
In one sentence: a "soft restart" without restarting the process, accounting for lineage analysis, surviving background tasks, and the hook lifecycle.
/compact — dispatch a fork agent to write a summary
Entry compact/compact.ts:40, the flow:
- Project away snipped messages; a user argument becomes a custom summary instruction (
:46-52); - With no custom instruction, first try session-memory compaction (a cheaper incremental path,
:57-83); - Traditional path: first microcompact (in-place compact of old tool results), then
compactConversation(); - PreCompact hooks can inject extra summary instructions; on prompt-too-long, drop the oldest turns and retry, up to
MAX_PTL_RETRIEStimes (:460-491); - Fork-agent cache-sharing path (
:1151-1203):runForkedAgentdispatches a sub-agent that runs only 1 turn (maxTurns: 1) with no tools, carrying a system prompt / tools / message prefix identical to the main thread so it hits the prompt cache — the source comment states the motive: the old path had a 98% cache miss, wasting about 38B tokens a day. On failure, fall back to ordinary streaming (:1280-1326); - Summary template
BASE_COMPACT_PROMPT(services/compact/prompt.ts:61): 9 fixed sections (Primary Request / Key Technical Concepts / Files and Code Sections / Errors and fixes / Problem Solving / All user messages / Pending Tasks / Current Work / Optional Next Step); buildPostCompactMessagesrebuilds the message sequence: compact boundary + summary + kept recent messages.
In one sentence: it does not delete messages; it "uses a cache-friendly fork sub-agent to write the old conversation into a structured summary, then rebuilds the session with that summary as the new foundation."
/init — an extra-long "interview script"
Entry commands/init.ts:226 (prompt type). The command itself does not scan a single line — it only returns a prompt and lets the main model do the work:
- Old version (
:6-26): have the model analyze the codebase and write CLAUDE.md, with a clear "should write" (build/lint/test commands, high-level architecture) and "should not write" (obvious advice, a per-file inventory); - New version (
:28-224): an 8-stage script — P1 AskUserQuestion about building project/personal files → P2 dispatch a sub-agent to survey the codebase → P3 interview the user to fill blinds → P4/5 write files (every line passes a "if we deleted this, would Claude make a mistake?" test) → P6 create skills → P7 create hooks → P8 summarize.
In one sentence: the extreme form of a prompt-type command — "the command is the script." That also answers "why /init costs tokens": it is in essence a long prompt sent to the model.
/login — PKCE + loopback listener, dual channel
Entry login/login.tsx:19 → ConsoleOAuthFlow (components/ConsoleOAuthFlow.tsx):
sequenceDiagram
participant CLI as Claude Code CLI
participant 浏览器
participant AS as Anthropic 授权服务器
CLI->>CLI: ① 生成 PKCE code_verifier/code_challenge + state
services/oauth/client.ts:47-87
CLI->>CLI: ② 起临时 localhost HTTP 服务器监听 /callback
auth-code-listener.ts:10-16
CLI->>浏览器: ③ 打开授权 URL(带 code_challenge)
浏览器->>AS: 用户登录并授权
AS-->>浏览器: 302 跳回 localhost:PORT/callback?code=...
浏览器-->>CLI: ④ 回环通道自动捕获 code,校验 state
Note over CLI: 3 秒没跳转?显示手动粘贴框(:157-181)
——SSH 无浏览器环境也能完成
CLI->>AS: ⑤ exchangeCodeForTokens 换 token(:103-109)
AS-->>CLI: access_token + refresh_token
CLI->>CLI: ⑥ installOAuthTokens 存安全存储(cli/handlers/auth.ts:44)
⑦ 收尾:重建客户端、剥旧 thinking 签名、
resetCostState、authVersion+1(login.tsx:25-55)
In one sentence: standard OAuth2+PKCE, dual channel (loopback automatic / manual paste) so an SSH environment with no browser can still finish login. Chapter 11 will cover where the token is stored and how expiry is renewed.
/resume — conversation archaeologist; across projects, prefer a command over crossing the boundary
Entry resume/resume.tsx: with no argument, getWorktreePaths + loadSameRepoMessageLogs + filterResumableSessions render LogSelector (supports semantic search); after a pick, checkCrossProjectResume decides — same-repo worktree resumes directly; a different project is not resumed, instead a claude --resume ... command is concatenated and copied to the clipboard (:145-164), for you to run in the new directory yourself. With an argument, first exact UUID match then search by title; what it reads is the ~/.claude/projects/<project>/<sessionId>.jsonl transcript file.
Why "prefer a command over crossing the boundary" across projects? A session is bound to a working directory and project context; switching projects in place would make one session serve two repos at once — the security boundary and the memory system would both go wrong. Giving you a command to start over on the other side is the most restrained and safest move.
/permissions — five tabs + retry a denied operation
Entry permissions/permissions.tsx: PermissionRuleList has five tabs (recent/allow/ask/deny/workspace); rules come from layered settings and are tagged with their source (user/project/local); applyPermissionUpdate takes effect in memory + persistPermissionUpdate writes the file; it also detects "shadowed rules" (you added a rule that can never fire, and it reminds you). The most interesting piece is onRetryDenials: wrap recently denied commands as createPermissionRetryMessage and stuff them back on the queue so the model retries — after you allow an operation on the permissions page, you do not have to restate the task; the model picks it up and tries again.
/cost — a purely local ledger
Entry cost/cost.ts:6: subscribers see a subscription hint; API users formatTotalCost() (cost-tracker.ts:228) output total cost/duration/lines added-deleted/token usage grouped by model (including cache read/write). Data all comes from in-process accumulators; no network request at all — typical of a local command: a pure function formatting in-memory counters. So /cost is always instant, and costs no tokens.
/commit — the textbook prompt command (internal-only in this snapshot)
Entry commands/commit.ts:76; it takes the three tricks of a prompt-type command to the extreme:
flowchart TD
A["你敲 /commit"] --> B["提示词里嵌四处 !`...` 预执行:
git status · git diff HEAD ·
git branch --show-current · git log --oneline -10
commit.ts:16-19"]
B --> C["本地先执行这 4 条命令
输出替换进提示词文本"]
C --> D["展开后的完整提示词发给模型:
「这是仓库现状,请按安全协议提交」"]
D --> E["安全协议约束模型:
不改 git config · 不跳 hooks · 不 amend
不提交疑似密钥"]
D --> F["allowedTools 白名单:
只放行 git add/status/commit"]
D --> G["两段式任务:先起草聚焦 why 的 message
再 HEREDOC 单条消息 stage+commit+署名"]
In one sentence: pre-execute to get the current state, a whitelist to govern the hands, a safety protocol to govern the floor — every move a custom command can learn is on this page.
9.4 A custom-command tutorial: write your own /review-fix
However strong the built-ins, nothing is as handy as one you wrote. Custom commands have two formats and four load locations.
Load locations and formats (skills/loadSkillsDir.ts:638-803):
| Location | Format | Notes |
|---|---|---|
~/.claude/commands/*.md |
old format (:566 tagged commands_DEPRECATED) |
a single file is a command |
~/.claude/skills/<name>/SKILL.md |
new format | directory + SKILL.md, can carry attachment files |
<project>/.claude/commands/*.md |
old format | project-level, shared with the team via git |
<project>/.claude/skills/<name>/SKILL.md |
new format | directory format only; a lone .md is not treated as a skill (:424-428) |
(There is also the enterprise managed-policy directory and --add-dir extra directories; plugin commands go through loadPluginCommands.ts:414.)
Namespace: a subdirectory becomes a colon prefix — .claude/commands/frontend/lint.md → /frontend:lint (buildNamespace :523-552); plugin commands carry a plugin:command prefix. Same names: first realpath wins (:725-763).
Template capabilities (getPromptForCommand :344-399):
- Argument substitution (
utils/argumentSubstitution.ts):$ARGUMENTSall arguments;$ARGUMENTS[0]/$0the Nth; after frontmatterarguments: ticketId priorityyou can use the named$ticketId; with no placeholder at all, automatically append\n\nARGUMENTS: <args>at the end (:140-142). - Shell pre-execution (
utils/promptShellExecution.ts:69): two syntaxes, inline!`cmd`and block-level```!; before sending to the model, executed by BashTool, stdout/stderr substituted into the text; execution first goes throughhasPermissionsToUseTool; the skill'sallowed-toolsis temporarily merged into always-allow. Safety red line: skills loaded from MCP always skip shell pre-execution (:371-374) — remote content is untrusted; a network server must not be allowed to run commands on your machine. - Variable injection:
${CLAUDE_SKILL_DIR}(the skill's own directory),${CLAUDE_SESSION_ID}. - How it runs: default inline expansion into the current conversation; with frontmatter
context: forkit runs as a sub-agent (executeForkedSlashCommand,processSlashCommand.tsx:62), optionally withagent:to specify the sub-agent type.
Frontmatter field cheat sheet (parseSkillFrontmatterFields :185-265):
| Field | Role |
|---|---|
| description | one-sentence description (default extracted from the body) |
| argument-hint / arguments | argument hint / named arguments |
| allowed-tools | whitelist of tools available while the skill runs |
| when_to_use | tell the model which scenes should invoke it |
| model / effort | specify a model (may inherit) / thinking effort |
| disable-model-invocation | humans only; the model cannot invoke |
| user-invocable: false | the user does not see it in the / menu |
| context: fork / agent | run in a forked sub-context / specify a sub-agent |
| paths | conditional skill: activated when a matching-path file is read/written |
| shell | which interpreter embedded shell commands use (e.g. powershell) |
| hooks / version / name | skill-level hooks / version / display name |
A complete example: write a /review-fix command. Need: have Claude review the current git changes against team convention and fix the problems directly. Create .claude/commands/review-fix.md:
---
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` 确认改动范围没有越界。
安全要求:不要提交,不要推送。
After you type /review-fix critical, the full lifecycle is (getMessagesForPromptSlashCommand at processSlashCommand.tsx:827): register skill hooks → addInvokedSkill records → generate a command-metadata message + isMeta expanded body (at this point the two !`...` pre-executions run) → parse @file attachments → attach a command_permissions attachment → shouldQuery: true send to the model.
9.5 Immediate commands: why some commands do not queue
While the model is busy writing code, an ordinary message you type goes on the queue (chapter 4). But a purely local UI command like /theme, /status waiting for the model to finish feels terrible — you only wanted to change the theme. Hence immediate: true: on a hit, load().call() runs directly, bypassing the queue pipeline (REPL.tsx:3158-3184), and reports tengu_immediate_command_executed.
- Which commands have it:
/btw,/color,/exit,/hooks,/mcp,/plugin,/rename,/sandbox,/status,/brief;/model,/effort,/fastare conditionally enabled (shouldInferenceConfigCommandBeImmediate,utils/immediateCommand.ts:11). - A keybinding trigger is always immediate: whatever command a shortcut evokes, it is treated as immediate — the keybinding itself is an explicit signal of "I want this now."
- Bridge safety whitelist: commands coming from phone/web have a separate
isBridgeSafeCommand(commands.ts:672) gate:local-jsxis always refused (the remote has no terminal to render a picker),promptis admitted,localneeds a whitelist. This defense comes from the lesson of PR #19134: the phone used to be able to trigger a local Ink picker dialog — the dialog appeared in a terminal you could not see, and the session hung.
🔍 Source pointers:
src/commands.ts:225,258,449,688,619,651(register/lookup/safety lists) |src/utils/processUserInput/processSlashCommand.tsx:525,62,827(dispatch/fork/expand) |src/commands/conversation.ts:49(/clearsoft restart) |src/commands/commit.ts:16-19,76(pre-execute + whitelist) |src/skills/loadSkillsDir.ts:185-265,344-399,523-552,638-803(frontmatter/template/namespace/load locations) |src/utils/promptShellExecution.ts:69,371-374(pre-execute and the MCP red line) |src/screens/REPL.tsx:3158-3184(immediate)
10. Ecosystem components: what plugins, skills, and MCP each are
However rich the built-in tools and commands, they cannot hold the world's demand. Claude Code left three doors open to the outside: plugins (package and distribute a whole set of capabilities), skills (spec sheets pulled on demand), MCP (a standard interface to plug in external services). The three often appear packaged together, but they solve completely different problems — section 10.4 has a comparison table.
10.1 Plugins: a "capability pack" installed all at once
Metaphor: a plugin is like a VS Code extension. A single .claude/commands/xxx.md file is too scattered; you cannot conveniently share and version a whole workflow. A plugin packs slash commands, skills, subagent definitions, hooks, MCP server config, and output styles into one directory, all installed in one shot.
Directory convention (file-header comments at utils/plugins/pluginLoader.ts:16-28; createPluginFromPath() at :1357):
flowchart TD
subgraph 插件目录["📦 一个插件 = 一个目录"]
M["plugin.json
清单文件(名字/版本/描述)"]
C["commands/
斜杠命令 *.md"]
A["agents/
subagent 定义"]
S["skills/
技能 SKILL.md"]
H["hooks/hooks.json
钩子脚本配置"]
O["output-styles/
输出样式 *.md"]
end
M --> L["createPluginFromPath 加载"]
C --> L
A --> L
S --> L
H --> L
O --> L
L --> P["commands/agents/skills/output-styles
四个可选目录并行探测是否存在
pluginLoader.ts:1392-1398"]
A marketplace = a git repo + marketplace.json. A marketplace is essentially a git repo (or a URL/NPM package) containing a marketplace.json catalog that registers which plugins it provides. The official marketplace is the GitHub repo anthropics/claude-plugins-official (utils/plugins/officialMarketplace.ts:13-27). The full flow:
/plugin marketplace add anthropics/claude-plugins-official→addMarketplaceSource()(marketplaceManager.ts:1782);gitCloneuses a partial clone + sparse checkout to clone the repo into~/.claude/plugins/marketplaces/(:803,1034 reconcileSparseCheckout) — a marketplace repo may have dozens of plugins; a sparse clone only pulls the few you install, saving bandwidth and disk;/plugin install feature-dev@claude-plugins-official→installPluginFromMarketplace()(pluginInstallationHelpers.ts:506) lands it;- On next startup
loadAllPlugins()(pluginLoader.ts:3096) loads uniformly; plugin commands appear in the/list, source-tagged as plugin.
Load priority: the command-merge order of chapter 9 — bundled skills → built-in plugin skills → directory skills → workflows → plugin commands → plugin skills → hard-coded built-in commands (commands.ts:447-470). findCommand takes the first match, so a plugin command can override a hard-coded built-in.
Built-in plugins: shipped with the CLI, toggleable in the /plugin UI (plugins/builtinPlugins.ts:21-32), IDs of the form {name}@builtin, no filesystem path (path: 'builtin' sentinel, :85); on/off state persisted to settings enabledPlugins (:71-76). The UI lives in commands/plugin/ (ManagePlugins.tsx, BrowseMarketplace.tsx, AddMarketplace.tsx, and 10+ other sub-UIs).
10.2 Skills: spec sheets pulled on demand
Metaphor: stuffing every tool's full manual into the drawer (the system prompt) is too heavy. So Claude Code only pastes a one-page table of contents on the drawer (skill name + one-sentence description); when the model decides it needs a particular manual, it pulls the whole book out via the Skill tool. That is progressive disclosure.
sequenceDiagram
participant 软件
participant 模型
Note over 软件: 第一阶段:只注入清单(省 token)
软件->>软件: getSkillListingAttachments
attachments.ts:2661
所有技能的 name/description/whenToUse
打包成一个 skill_listing 附件,按 token 预算裁剪
软件->>模型: 请求里夹带技能清单(每个 agent 只发一次,增量补发 :2698-2732)
Note over 模型: 用户说"把 report.pdf 第 3 页的表抠出来"
→ 清单里有 pdf 技能,决定使用
模型-->>软件: tool_use: Skill {skill: "pdf", args: "..."}
Note over 软件: 第二阶段:此刻才加载全文
软件->>软件: getPromptForCommand(loadSkillsDir.ts:344-399):
拼入 Base directory + SKILL.md 完整正文
替换 $ARGUMENTS/${CLAUDE_SKILL_DIR}/${CLAUDE_SESSION_ID}
执行内嵌 !`shell`(MCP 技能除外)
软件-->>模型: 完整技能说明书(含 scripts/extract.py 的存在)
模型-->>软件: tool_use: Bash 跑提取脚本
Key details:
- Format: only the
skill-name/SKILL.mddirectory layout is recognized; a lone.mdfile is not treated as a skill (loadSkillsDir.ts:424-428). - Four load locations (
:638-714): enterprise-managed → user-level~/.claude/skills→ project-level.claude/skills(including--add-dir) → the legacy/commands/directory. - Frontmatter fields: name, description, when_to_use, allowed-tools, argument-hint/arguments, version, model, effort, disable-model-invocation, user-invocable, hooks, context: fork, agent, shell, paths (parse entry
parseSkillFrontmatterFields :185-265; see the field table in section 9.4). - Conditional skills (
paths):parseSkillPaths :159-178supports gitignore-style path filtering — activated only when a matching-path file is read/written (fromactivateConditionalSkillsForPaths :997). E.g. a skill withpaths: ["**/*.py"]only appears in the listing when a Python file shows up. - Dynamic discovery: while a session reads/writes files, nested
.claude/skillsare discovered by walking up from the file path (discoverSkillDirsForPaths :861-915), skipping gitignored directories; a skill in a deeper directory overrides a same-named shallower one (:944-951). - Built-in bundled skills: compiled into the binary (
skills/bundledSkills.ts:53), directoryskills/bundled/:batch,claudeApi,claudeInChrome,debug,keybindings,loop,loremIpsum,remember,scheduleRemoteAgents,simplify,skillify,stuck,updateConfig,verify. Skills with attachments unpack reference files to disk only on first invoke, withO_NOFOLLOW|O_EXCLagainst symlink attacks (:176-193). - Relation to the old commands: the old format
.claude/commands/*.mdis tagged DEPRECATED and is still compatible under the same load logic; the safety red line once more — skills from MCP never execute embedded shell pre-execution (:372-374); remote content is untrusted.
Example: write your own skill. Create ~/.claude/skills/weekly-report/SKILL.md:
---
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。
From next week on, you say "help me write the weekly report," and the model finds this spec sheet itself and follows it.
10.3 MCP: USB-C for AI
Metaphor: MCP (Model Context Protocol) is "USB-C for AI" — any external service (a database, Figma, GitHub, an internal company system) that implements a server per the protocol can be plugged into Claude Code, and its tools, resources, and prompt templates become immediately usable. The client implementation is services/mcp/client.ts (3348 lines), based on the official @modelcontextprotocol/sdk.
6 transports (schema in services/mcp/types.ts:28-131):
| Transport | Scene | Implementation |
|---|---|---|
stdio |
local subprocess (most common: npx starts a process) |
client.ts:950 |
sse |
HTTP long-poll push | client.ts:673,702 |
http |
Streamable HTTP | client.ts:861,900 |
ws / ws-ide |
WebSocket / embedded in an IDE | client.ts:734,783 |
sdk |
in-process SDK transport | client.ts:3278 |
| others | special types sse-ide, claudeai-proxy, etc. |
— |
Config and startup approval: servers are registered in .mcp.json at the project root (enterprises also have managed-mcp.json, services/mcp/config.ts:64-66):
{
"mcpServers": {
"github": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] }
}
}
Why does an untrusted repo need approval? Same logic as chapter 3's trust dialog: .mcp.json is project-level config, possibly from a strange repo you just git cloned — if clone automatically started servers, a malicious repo could automatically run processes on your machine. So at startup each pending server pops an approval dialog (handleMcpjsonServerApprovals(), services/mcpServerApproval.tsx:13-40); your choice is recorded in settings enabledMcpjsonServers/disabledMcpjsonServers (from utils.ts:351); load time only admits approved ones (config.ts:1164-1168). --dangerously-skip-permissions and non-interactive mode auto-approve, but that code carries a strict anti-RCE comment (utils.ts:383-400).
Three kinds of capability: ① tools — wrapped as MCPTool named mcp__serverName__toolName (section 8.16); ② resources — browsed/read with ListMcpResourcesTool/ReadMcpResourceTool; ③ prompts — turned into Commands (client.ts:2038-2077, names likewise mcp__server__prompt format). Permission rules support server-level wildcards: one rule mcp__github or mcp__github__* governs the whole server (utils/permissions/permissions.ts:236-259).
Connection state machine (types.ts:183-248): the management UI is the /mcp command; the React side is MCPConnectionManager + useManageMCPConnections for reconnect and on/off:
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: when a server needs login authorization it enters needs-auth, takes the standard OAuth flow (the dynamically registered McpAuthTool walks you through it), and returns to connected after success.
10.4 Plugin vs skill vs MCP: one comparison table
| Dimension | Plugin | Skill | MCP |
|---|---|---|---|
| Problem it solves | packaging, distributing, and versioning a whole workflow | teaching the model "the SOP for a class of work," loaded on demand to save tokens | plugging in an external service's live data and capabilities |
| Shape | one directory (may contain commands/skills/agents/hooks/MCP config) | one SKILL.md (+ optional attachments) | one external server process/service |
| Who can provide tools | indirectly (via embedded MCP config) | cannot — it is only a prompt | can — mcp__server__tool |
| Trust boundary | you looked at the marketplace/repo before install | local files are trusted; skills from MCP are not (shell pre-execution forbidden) | project .mcp.json needs startup approval |
| Typical source | a git marketplace (e.g. official anthropics/claude-plugins-official) |
you write it / a plugin ships it / built-in bundled | a third-party vendor or you build it |
| Management entry | /plugin | /skills | /mcp |
One-sentence distinction: a skill is a "spec sheet," MCP is a "new organ," a plugin is "the gift box that packages the spec sheet and the new organ for sale."
🔍 Source pointers:
src/utils/plugins/pluginLoader.ts:1357,1392-1398,3096(plugin load) |src/utils/plugins/marketplaceManager.ts:803,1034,1782(sparse clone / add marketplace) |src/skills/loadSkillsDir.ts:185-265,424-428,638-714,997(skill parse/load/conditional activation) |src/utils/attachments.ts:2661(skill-listing attachment) |src/services/mcp/client.ts:1743-1832,2038-2077(tool/prompt wrapping) |src/services/mcpServerApproval.tsx:13-40(startup approval) |src/services/mcp/types.ts:28-131,183-248(transports and state machine)
11. Feature teardown: effort, login, model switching, …
The first ten chapters covered the spine. This chapter teardowns the "feature switches" scattered around: each item in the same pattern — what it is (a metaphor) → how the source implements it (line numbers) → one example.
11.1 effort / thinking budget: a grade for the model's "drafting time"
What it is: before answering, the model can "draft" (thinking), and the draft length is tunable. effort is the new-generation unified knob (four grades low/medium/high/max); thinking budget is the old-style exact token count; the Claude 4.6 family also supports adaptive — the model decides how long to think. Metaphor: the same person, you can have them "answer offhand" (low) or "think it through then speak" (max).
How the source implements it:
flowchart TD
A["resolveAppliedEffort 优先级链
utils/effort.ts:152-168"] --> B{"① 环境变量
CLAUDE_CODE_EFFORT_LEVEL?"}
B -->|有| F["✅ 用它"]
B -->|无| C{"② 会话状态
(/effort 或 /model 设的
appState.effortValue)?"}
C -->|有| F
C -->|无| D["③ 模型默认值"]
D --> F
F --> G{"clamp:模型支持吗?
modelSupportsEffort :26-50"}
G -->|"max 仅 Opus 4.6 支持
(:55-69)"| H["不支持 max → 自动降到 high"]
G -->|支持| I["发给 API:
outputConfig.effort(claude.ts:453-455)
数字档走内部 effort_override(:458-463)"]
H --> I
- Grades are defined in
utils/effort.ts:14-20(also allows a numeric grade, used internally); the/effortcommand is incommands/effort/effort.tsx:16-20; on persist, numeric grades and an external user'smaxare not written to settings (toPersistableEffort :101-112). - Two shapes of thinking (
utils/thinking.ts:10-13): the 4.6 family sends{type: 'adaptive'}with no budget; older models send{type: 'enabled', budget_tokens: N}, budget =min(maxOutputTokens-1, configured value ?? model default)(claude.ts:1617-1627). On by default;MAX_THINKING_TOKENS=0or settingsalwaysThinkingEnabled: falsecan turn it off (:146-162). - Easter egg: writing
ultrathinkin the input triggers a highlight and a higher thinking grade (keyword detection atthinking.ts:29-31+ rainbow render:60-86), double-gated by feature flagULTRATHINK+ GrowthBooktengu_turtle_carbon.
Example: you /effort high; the next request's API body has output_config: {effort: 'high'}; if the model is Opus 4.6, the thinking field is {type: 'adaptive'}; the status bar shows the grade in sync.
11.2 API login and authentication: four ways to pay, and a locked token refresh
What it is: one SIM can ride a contract plan, a prepaid card, or a company leased line — Claude Code supports four "ways to pay": a Claude subscription account (OAuth), an API key billed by usage, enterprise cloud channels (AWS Bedrock / Google Vertex / Azure Foundry).
How the source implements it:
- Provider detection (
utils/model/providers.ts:6-13): look at envCLAUDE_CODE_USE_BEDROCK/CLAUDE_CODE_USE_VERTEX/CLAUDE_CODE_USE_FOUNDRY; if none are set, it isfirstParty. - OAuth flow: the
/logindeep dive of section 9.3 already covered it (PKCE + loopback listener, dual channel,services/oauth/index.ts:14-120). - Credential storage: on macOS the OAuth token lives in the Keychain (
utils/secureStorage/index.ts:9-17, falling back to a plaintext file on failure). API-key source priority (utils/auth.ts):apiKeyHelper(an executable configured in settings,:123) →ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKENenv (:126,134) →--settingsinjection → keychain. - Locked token refresh (
checkAndRefreshOAuthTokenIfNeeded,auth.ts:1427-1560): on finding an expiredaccessToken, ① first clear the cache and re-read disk (another process may already have refreshed); ② if a real refresh is needed, grab a file lock (lockfile.lock :1500) against concurrent refresh by multiple processes; ③ retry up to 5 times (:1453); ④ a subscriber refresh omits scopes so the server can expand them (:1544-1550). On 401/"token revoked", the retry layer forces a refresh (withRetry.ts:246-252). - Billing header and cache-scope split: the first block of every request's system prompt is
x-anthropic-billing-header: cc_version=...; cc_entrypoint=...; cch=...(constants/system.ts:86-95).splitSysPromptPrefix()(utils/api.ts:321-376) lifts it out of the system prompt and markscacheScope: null(does not participate in the prompt cache); remaining static blocks useglobal/orgcache scopes — changing entrypoint (CLI/SDK) does not break the cache, a typical piece of careful accounting.
Example: after you /login and authorize, the token goes into the macOS Keychain; an hour later it expires; checkAndRefreshOAuthTokenIfNeeded takes the file lock and silently renews — two terminals both running Claude Code will not double-refresh; you notice nothing.
11.3 Model selection and fallback: lead actor and understudy
What it is: normally use the primary model; when Anthropic's servers are overloaded (HTTP 529), automatically swap in the backup so the conversation does not break.
How the source implements it:
- Primary-model priority chain (
getMainLoopModelatutils/model/model.ts:84-101): ① what/modelchanged in-session (bootstrap/state.ts:838) → ② startup flag--model→ ③ envANTHROPIC_MODEL→ ④modelin settings → ⑤ built-in default (Opus 4.6; third-party channels default Sonnet 4.5 because listing lags, the@[MODEL LAUNCH]comment at:103-133). - fallback specification:
--fallback-model <model>(main.tsx:1000, note the help text says "only works with --print"), and it is not allowed to be the same as the primary (:1337validation). - Trigger and switch:
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
清空本轮半成品 assistant 消息
(补齐 'Model fallback triggered' 工具结果)
query.ts:894-910
循环->>重试层: 用 Sonnet 重发整个请求
重试层->>API: 请求(fallback 模型)
API-->>重试层: ✅ 正常回复
A detail: by default this only fires for "non-subscribers on non-custom Opus"; set FALLBACK_FOR_ALL_PRIMARY_MODELS to open it up (:330-336); background tasks (summaries, title generation) on 529 are dropped rather than retried with backoff (:319-327).
- Small-model division of labor: getSmallFastModel() (claude.ts:339-340) — conversation-topic detection, command descriptions, and other chores use a Haiku-class small model, separated from the main-loop model. Good steel on the cutting edge.
Example: claude -p --model opus --fallback-model sonnet "重构这个模块". At peak, Opus returns 529 three times in a row; the loop silently switches to Sonnet and continues; you only see a model-switch notice in the transcript.
11.4 Six permission-mode grades: how long a leash the AI gets
What it is: from "ask me every step" to "fully off-leash," several grades, cycled with Shift+Tab.
Semantics of the six grades (utils/permissions/PermissionMode.ts:42-91):
| Mode | Semantics |
|---|---|
default |
ask via dialog on every dangerous operation |
plan |
research and propose only; must not touch files / run commands; at the end, use ExitPlanMode to hand the plan to you for approval (write operations intercepted from utils/permissions/filesystem.ts:1446) |
acceptEdits |
file edits inside the working directory are auto-allowed (filesystem.ts:1366); everything else still asks |
bypassPermissions |
skip all asking (dangerous; needs --dangerously-skip-permissions or a UI confirmation to enable) |
dontAsk |
not exposed in the UI cycle (getNextPermissionMode.ts:70-72) |
auto |
internal (ant-only) mode: a transcript classifier judges in real time whether each operation is safe (feature flag TRANSCRIPT_CLASSIFIER, PermissionMode.ts:80-90) |
Shift+Tab cycle (keybinding keybindings/defaultBindings.ts:30-36; on Windows without VT mode, falls back to meta+m):
flowchart LR
D["default"] --> A["acceptEdits"] --> P["plan"] --> B["bypassPermissions
(若可用)"] --> D
style P fill:#bde0fe
style A fill:#caffbf
style B fill:#ffc6ff
Order is defined in getNextPermissionMode.ts:34-79; on switch, cyclePermissionMode() (:88-101) calls transitionPermissionMode to clean context (e.g. strip dangerous permissions when entering auto); the status bar changes color by mode (plan blue, accept cyan, bypass red, PermissionMode.ts:26-30).
Example: you want Claude to research a bug but fear it will thrash the code: Shift+Tab to plan; Claude reads code, writes a plan, and finally ExitPlanMode pops "approve the plan?"; after approval it auto-switches to acceptEdits and lands the edits (filesystem.ts:1450 mode suggestion).
11.5 Output styles: a "personality mask" for Claude
What it is: an output style directly rewrites the opening of the system prompt so the same model answers in a different style — didactic vs. minimal, like changing a personality mask.
How the source implements it:
- Built-in styles (
constants/outputStyles.ts:42-100):default(value null, no extra seasoning),Explanatory(explain every implementation choice, with a★ Insightblock template),Learning(deliberately leave 2–10 lines as a small task for you to write, theTODO(human)mechanism). - Custom styles: drop a markdown file into
~/.claude/output-styles/*.mdor the project's.claude/output-styles/*.md; frontmatter supportsname,description,keep-coding-instructions(loadOutputStylesDir.ts:41-62); plugins can also provide them via anoutput-styles/directory. - Injection location: the selected style is concatenated as a
# Output Style: {name}\n{prompt}paragraph into the system prompt (constants/prompts.ts:152-157); the thesis sentence becomes "according to your Output Style below" (:180).keep-coding-instructions: truemeans keep the original coding instructions and only overlay style.
Example: after /output-style Learning, when Claude writes 30 lines of code it deliberately leaves the core function and sends a "Learn by Doing" card asking you to fill in the TODO(human) part — learn while you use.
11.6 The memory system: four "brains"
What it is: long-term memory across sessions (MEMORY.md auto-memory), the project covenant (the CLAUDE.md family), notes for the current session (Session Memory), a manual editor (/memory) — four layers.
How the source implements it:
- Auto memory (memdir):
memdir/memdir.ts— one memory directory per project (default~/.claude/projects/<project-path>/memory/); entry fileMEMORY.mdhard-capped at 200 lines / 25KB, truncated with a warning when over (memdir.ts:35,37-40,59-95). Claude autonomously writes user preferences and project habits into markdown files under that directory; at startup they are retrieved by relevance and injected (findRelevantMemories.ts); injected entries carry a "freshness" prefix (memoryFileFreshnessPrefixatFileReadTool.ts:749). When you correct Claude, the tail of the denial message also appends a hint to "consider storing this in memory" (utils/messages.ts:177-191). - Session Memory:
services/SessionMemory/sessionMemory.ts:1-7— a background fork of a sub-agent periodically distills conversation points into notes, without interrupting the main dialogue; used to restore context after auto-compact. - CLAUDE.md system: chapter 5 already covered it in detail (collection order,
@include, identity tags); a subdirectory's CLAUDE.md is injected on demand when a file in it is read (attachments.ts:1710+FileReadTool.ts:848,870). - The
#quick-memory shortcut no longer exists: stated clearly — this source snapshot has no#-prefix input mode:inputModes.ts:31-33only recognizes!(bash mode), and thePromptInputModetype (types/textInputTypes.ts:265-269) only has bash/prompt etc. The early-version#quick-append memory has evolved into the/memorycommand + auto-memory system — the#usage in old tutorials on the web will not work in this version; note the version difference.
Example: you say offhand "our team writes commits in Chinese"; Claude writes that into memory/team-conventions.md and updates the MEMORY.md index; next week's new session, that preference shows up automatically in the system prompt with the relevant memories.
11.7 A quick look at other mechanisms
| Mechanism | One sentence | Key implementation |
|---|---|---|
| Vim mode | the input box supports NORMAL/INSERT switch, motion, operator, text object | pure-function modules vim/motions.ts, operators.ts, textObjects.ts, transitions.ts; state types textInputTypes.ts:255-260 |
| Voice input | push-to-talk: connect to Anthropic's voice WebSocket for streaming transcription; must have an OAuth token (uses the claude.ai endpoint; API key/Bedrock/Vertex unavailable) | services/voiceStreamSTT.ts:1-36; gate voice/voiceModeEnabled.ts:18-24 (GrowthBook kill switch) + :31-40 hasVoiceAuth |
| Remote / bridge | this machine becomes the "executor" for the claude.ai web/App: poll the cloud for tasks, run them locally, post results back; permission dialogs can be synced to the web for confirmation | bridge/bridgeMain.ts; remote/SessionsWebSocket.ts, remotePermissionBridge.ts |
| cost tracking | each API response's usage is converted to dollars per the model price list and accumulated; /cost displays; persisted across sessions |
cost-tracker.ts:3-27,146; utils/modelCost.js |
| telemetry | events uniformly go through logEvent(), names of the form tengu_*; sensitive fields forced through a type tag (..._I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS) |
services/analytics/ (Statsig/GrowthBook/Datadog/first-party reporting) |
| Keybinding system | declarative bindings: the default table is chunked by context; users can override in keybindings.json; ctrl+c/ctrl+d are reserved and cannot be rebound |
keybindings/defaultBindings.ts:37+, loadUserBindings.ts, parser.ts/match.ts |
| UDS message service | at startup, create a Unix Domain Socket under $TMPDIR; an external process can write messages to inject them into the conversation; the bind must finish before the SessionStart hook |
setup.ts:86-101 (feature UDS_INBOX, skipped by --bare) |
--worktree isolation |
the session runs inside a git worktree (.claude/worktrees/<slug>); changes do not touch the main workspace; the slug has path-traversal protection |
entrypoints/cli.tsx:247-261; utils/worktree.ts:52-55 |
| SDK / headless | claude -p non-interactive mode: no React tree; supports --output-format json/stream-json; QueryEngine abstracts "one query" into an engine shared by SDK and CLI |
cli/print.ts (5594 lines, comment :517); QueryEngine.ts:370,518 |
--bare |
slim startup: skip skill discovery, UDS, telemetry init, etc.; what you want is speed | loadSkillsDir.ts:654-675, setup.ts:86-88 |
🔍 Source pointers:
src/utils/effort.ts:14-20,152-168(effort) |src/utils/auth.ts:1427-1560(token refresh) |src/utils/api.ts:321-376(billing-header split) |src/utils/model/model.ts:84-101,src/services/api/withRetry.ts:54,163-165,src/query.ts:894-910(model and fallback) |src/utils/permissions/PermissionMode.ts:42-91,getNextPermissionMode.ts:34-79(permission modes) |src/constants/outputStyles.ts:42-100(output styles) |src/memdir/memdir.ts:35-95,src/services/SessionMemory/sessionMemory.ts(memory)
12. Memory and forgetting: context compaction
12.1 Why compact
As emphasized again and again: every turn's request carries the entire history. And the model's "brain capacity" (context window) is finite — say 200,000 tokens. A large refactor fills the conversation with hundreds of files and endless command output, and you hit the ceiling fast. Even if you do not hit it, longer history means slower responses, higher cost, and the model is more easily distracted by irrelevant old information.
12.2 Five layers of defense: a "slim-down check" at the start of every loop turn
At the start of every loop turn Claude Code runs five slim-down programs in sequence (src/query.ts:379-648):
flowchart LR
A["每圈循环开头"] --> B["① applyToolResultBudget
超大工具结果落盘存文件
历史里只留'摘要+路径'"]
B --> C["② snip
剪掉冗余片段"]
C --> D["③ microcompact
按 tool_use_id 清理
已完成使命的旧工具结果"]
D --> E["④ context collapse"]
E --> F{"⑤ 快到触发线了吗?"}
F -->|没到| G["✅ 正常请求模型"]
F -->|到了| H["autocompact 自动压缩"]
H --> G
The first four are "local weight loss" (no information loss, or very little); the fifth, autocompact, is the "major surgery."
12.3 The math of the trigger line: a 200k window, action around 190k
The trigger-line calculation (src/services/compact/autoCompact.ts:62-76) is simple arithmetic, but every number is deliberate:
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000 // 警戒线余量
// ……
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 // 熔断器
// 触发线 = effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
- Effective window = model window − min(max output, 20000): reserve space for the model's "answer"; a 200k window is actually about 180k usable;
- Trigger line = effective window − 13000: 180k − 13k ≈ action around 167k (under a 200k window and default output config, the felt sense is that it fires before 190k tokens). Why leave a 13000 buffer? Because the compact summary itself takes space, and "compact early" is much calmer than "hit the wall then rescue";
- Circuit breaker = give up after 3 consecutive failures: the source comments record a bloody lesson — there used to be 1279 sessions that failed 50+ times in a row (max 3272), wasting about 250,000 API calls a day globally, so a breaker was added. Compaction failure burns money; retries cannot be allowed to run away;
- Env
DISABLE_AUTO_COMPACTcan turn the whole thing off (then hitting the "window − 3000" blocking limit errors directly, leaving room for you to/compactby hand).
12.4 How compaction is done: dispatch a "one-shot clone" to write reading notes
The most interesting part is how compaction is done: not a crude chop of the first half, but dispatch a one-shot clone agent to read the whole conversation and write a summary (compactConversation at src/services/compact/compact.ts:387; internally runForkedAgent reuses the same query loop, querySource: 'compact', maxTurns: 1, one turn only).
The clone deliberately keeps the same system prompt, the same tools, the same model, the same message prefix — why? To hit the prompt cache: an identical prefix means most of this "write a summary" request's input can take the cache, saving money and time.
After the summary is written, buildPostCompactMessages (compact.ts:330) reassembles history:
flowchart TB
subgraph BEFORE["压缩前的历史(约 19 万 token)"]
O1["你的问题 1"] --> O2["AI 回复 + 几十个工具结果"] --> O3["你的问题 2"] --> O4["更多工具结果……"]
end
subgraph AFTER["压缩后的历史(可能只剩 2 万)"]
S["📝 摘要消息:分身写的'读书笔记'
(之前对话的要点总结)"]
BND["✂️ compact_boundary 压缩边界标记"]
R["边界之后的少量原始对话"]
S --> BND --> R
end
BEFORE ==>|compactConversation| AFTER
From then on getMessagesAfterCompactBoundary only takes content after the boundary plus that summary — like condensing a textbook into one page of notes before an exam, then reading only the notes and the last few chapters. Under a 200k window, one compaction usually takes 190k tokens down to twenty or thirty thousand, so the conversation can continue indefinitely (the cost is that detail before the boundary is gone, leaving only the points in the summary).
12.5 Reactive rescue: a 413 salvage after hitting the wall
If prevention failed and the API actually returned "content too long" (413), there is still reactive compaction (reactiveCompact): the software quietly hides the error (so you do not see a scary message; hasAttemptedReactiveCompact in State guarantees only one try), compact once in an emergency, and resend. Only when every means has failed does it tell you "the conversation is too long; please /compact or start a new conversation" (prompt_too_long termination).
flowchart TD
A["请求 API"] -->|413 prompt too long| B["withheld:错误先不显示"]
B --> C["reactiveCompact 紧急压缩一次"]
C --> D{"重试"}
D -->|成功| E["✅ 用户无感知,循环继续"]
D -->|失败| F["❌ 报 prompt_too_long
建议手动 /compact 或开新会话"]
A -->|正常| G["继续主循环"]
🔍 Source pointers:
src/query.ts:379-648(five layers of defense) |src/services/compact/autoCompact.ts:62-76,262,343(trigger line and breaker) |src/services/compact/compact.ts:330,387(boundary reassembly and clone summary)
13. Clones and the emergency brake: sub-agents and interrupt
13.1 Sub-agents: one engine, several run modes
When a task is large, the model can call the Agent tool to dispatch a "clone" onto a subtask (e.g. "go search the whole codebase for every use of this function"). Architecturally there is an elegant point (src/tools/AgentTool/runAgent.ts:248): the sub-agent runs the main loop itself — the same query() function (runAgent.ts:748), just with three things swapped: its own system prompt, a shrunk toolbox, and an identity tag (agentId is set, querySource: 'agent:...'). The main agent, the sub-agent, and the clone that writes summaries are all reuse of the same engine — which is also why the compact clone can "incidentally" hit the prompt cache.
The task system (src/Task.ts) defines seven task types: local_bash / local_agent / remote_agent / in_process_teammate / local_workflow / monitor_mcp / dream, with ID prefixes b/a/r/t/w/m/d respectively — the prefix of a task number you see in the UI already exposes its type.
13.2 Foreground vs background: two timings
A sub-agent has two run modes; the whole difference is "does the main loop wait":
sequenceDiagram
participant 主 as 主循环
participant 子 as 子 Agent
Note over 主,子: 前台(同步):主循环阻塞等待
主->>子: 派任务(runAgent 同步调用)
主-->主: ⏸️ 原地等待……
子->>子: 自己跑完整的主循环(可能转很多圈)
子-->>主: 最终结果作为 tool_result 返回
主->>主: ▶️ 继续下一圈
Note over 主,子: 后台(异步):主循环立刻继续
主->>子: 派任务(LocalAgentTask)
子-->>主: 立刻返回 async_launched
主->>主: ▶️ 继续干别的(可能又转了好几圈)
子->>子: 后台独立运行
子-->>主: 完成后往 messageQueueManager
塞一条 task-notification
主->>主: 下一圈读到通知,继续决策
Imagine asking an assistant to look something up: foreground is you sitting there until they come back; background is you continuing your own work, and they WeChat you when they are done. When to use which? Use foreground when the next step depends on the subtask's result (you cannot walk without waiting); use background when you just want some "on the side" work (run tests in the background, monitor).
There is also an experimental coordinator mode (feature('COORDINATOR_MODE')): the main agent is demoted to a "coordinator" that can only use the three tools Agent / SendMessage / TaskStop — it does not work itself, only dispatches work and collects results, like a project manager.
13.3 ESC: an emergency brake that goes all the way to the network cable
You can press ESC at any time to interrupt the model. The full chain of this "brake line" is:
flowchart LR
A["⌨️ 你按 ESC"] --> B["defaultBindings.ts:66
escape → chat:cancel"]
B --> C["useCancelRequest.ts:87
handleCancel"]
C --> D["abortController.abort()"]
D --> E["signal 一路传到
claude.ts 的 HTTP 层"]
E --> F["🔌 TCP 连接真正掐断
立刻停止计费"]
F --> G{"循环在哪个检查点发现?"}
G -->|流式接收中(:1015)| H["补孤儿 tool_result
return aborted_streaming"]
G -->|工具执行中(:1485)| I["补孤儿 tool_result
return aborted_tools"]
This emergency brake is thorough:
- The HTTP request is actually cut — it is not "pretend we did not see the result"; the connection is truly broken, billing stops immediately (the
signalpassed when issuing the request in section 6.4 is this brake line); - Two checkpoints wind down cleanly: cut mid-stream receive, or cut mid-tool execution; the loop first does the "patch orphan
tool_result" of section 6.6 (otherwise the conversation is wrecked), then exits asaborted_*; - ESC while idle has another use: it pops the next message out of the message queue.
13.4 Layers of cancel: why ESC cannot kill a background task
Cancel signals propagate by "parent-child layer" (createChildAbortController, parent → child, one way):
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
Why do background tasks deliberately not die with ESC? Because it would be counter-intuitive: you press ESC to stop "the work currently painting the screen," not to kill a background test you dispatched ten minutes ago that has already been running for a while — the cost of a mistaken kill is far greater than the bother of "one extra dedicated shortcut."
🔍 Source pointers:
src/tools/AgentTool/runAgent.ts:248,748(sub-agent reuses query) |src/Task.ts(seven task types) |src/keybindings/defaultBindings.ts:66(ESC binding) |src/hooks/useCancelRequest.ts:87(handleCancel) |src/query.ts:1015,1485(two interrupt checkpoints)
14. The full picture: one diagram of the whole path
String every previous chapter together and this is the complete journey after you send a prompt:
flowchart TD
subgraph 你的电脑["💻 你的电脑(Claude Code 软件)"]
A["⌨️ 敲字回车
useTextInput → PromptInput → REPL"] --> B["分流判断(纯字符串规则):
空? exit? !shell? /命令?"]
B -->|本地命令| C["⚙️ 本地执行
不发 AI"]
B -->|普通提问| D["📦 打包:系统提示词(+gitStatus)
+ <system-reminder>(CLAUDE.md+日期)
+ 全部历史 + 工具清单"]
D --> E{"🔁 queryLoop while(true)"}
E --> F["压缩防线五连
(落盘/snip/microcompact/collapse/autocompact)"]
F --> G["流式请求模型
裸 stream,SSE 大 switch"]
G --> H{"有 tool_use?"}
H -->|有| I["权限五关裁决
deny→ask→自查→模式→alwaysAllow→弹窗"]
I -->|被拒| J["拒绝原因回喂模型
(模型自我纠正)"]
I -->|放行| K["🔧 执行工具(边收边跑
只读并行/写操作串行)"]
K --> L["tool_result 追加进历史"]
J --> L
L --> E
H -->|没有| M["🎉 return completed
显示最终答案"]
N["ESC 急刹车
signal 直达 HTTP 层"] -.随时.-> G
N -.随时.-> K
end
G -.->|HTTPS stream:true| O["☁️ Anthropic 服务器
Claude 大模型"]
O -.->|SSE 事件流| G
P["🤖 子 agent / 压缩分身
(复用同一个 query())"] -.被 Agent 工具唤起.-> E
Appendix: source map
If you want to read the source yourself, here is a map of the files this lecture cites:
| Topic | File | What to look at |
|---|---|---|
| Program entry | src/entrypoints/cli.tsx |
the "sorter" main(), --version with zero loading |
| Global init | src/entrypoints/init.ts |
memoized init, preconnect API |
| Session init | src/setup.ts |
working directory, hooks snapshot |
| UI render | src/ink/, src/components/, src/screens/REPL.tsx |
terminal UI, input box |
| Key handling | src/hooks/useTextInput.ts |
Enter's four fates (:247) |
| Input routing | src/utils/handlePromptSubmit.ts, src/utils/processUserInput/ |
blank drop (:188), exit list (:196), shouldQuery |
| Slash commands | src/commands.ts |
findCommand exact match (:688) |
| Context assembly | src/constants/prompts.ts, src/context.ts, src/utils/api.ts |
getSystemPrompt (:444), placement (:437/:449) |
| Memory files | src/utils/claudemd.ts |
collection order (:790), @include (:618), identity tags (:1153) |
| Main loop (the heart) | src/query.ts |
queryLoop (:241), while(true) (:307), patch orphans (:123) |
| API transport | src/services/api/claude.ts |
raw stream (:1822), SSE switch (:1980-2303) |
| Tool definitions | src/Tool.ts, src/tools.ts |
buildTool (:783), getAllBaseTools (:193) |
| Tool execution | src/services/tools/toolExecution.ts, StreamingToolExecutor.ts |
runToolUse (:337), concurrency discipline |
| Permission system | src/utils/permissions/permissions.ts, dangerousPatterns.ts |
five-gate adjudication (:1158) |
| Hook system | src/utils/hooks.ts, src/services/tools/toolHooks.ts |
spawn execution, PreToolUse (:435) |
| Context compaction | src/services/compact/ |
trigger line (autoCompact.ts:62), clone summary (compact.ts:387) |
| Sub-agents | src/tools/AgentTool/runAgent.ts, src/Task.ts |
reuse query() (:748), seven task types |
| Interrupt | src/hooks/useCancelRequest.ts, src/keybindings/defaultBindings.ts |
ESC chain (:87/:66) |
| Tool field guide (chapter 8) | src/tools/ (39 tool directories) |
Bash compound-command split (BashTool.tsx:447-465), read-before-write (FileEditTool.ts:281), three-flag defaults (Tool.ts:750-761) |
| Tool delayed load | src/tools/ToolSearchTool/, src/utils/toolSearch.ts |
defer list (prompt.ts:62-108), search scoring (ToolSearchTool.ts:132-216) |
| MCP tool wrapping | src/services/mcp/client.ts |
fetchToolsForClient (:1743-1832), annotations→three flags (:1795-1809) |
| Slash commands (chapter 9) | src/commands.ts, src/commands/ |
COMMANDS registration (:258), merge order (:447-470), safety lists (:619/:651) |
| Command dispatch | src/utils/processUserInput/processSlashCommand.tsx |
getMessagesForSlashCommand (:525), fork execution (:62), prompt-command lifecycle (:827) |
| Representative commands | src/commands/conversation.ts, commands/commit.ts, commands/init.ts |
/clear soft restart (:49), pre-execute + whitelist (:16-19), /init script (:226) |
| Custom command / skill load | src/skills/loadSkillsDir.ts, src/utils/promptShellExecution.ts |
frontmatter (:185-265), namespace (:523-552), shell pre-execute red line (:371-374) |
| Immediate commands | src/screens/REPL.tsx, src/utils/immediateCommand.ts |
bypass the queue (:3158-3184) |
| Plugin ecosystem (chapter 10) | src/utils/plugins/pluginLoader.ts, marketplaceManager.ts |
directory-structure probe (:1392-1398), sparse clone (:803/:1034) |
| Skill progressive disclosure | src/utils/attachments.ts, src/skills/bundledSkills.ts |
listing attachment (:2661), bundled-skill registration (:53) |
| MCP ecosystem | src/services/mcpServerApproval.tsx, services/mcp/types.ts |
startup approval (:13-40), connection state machine (:183-248) |
| effort mechanism (chapter 11) | src/utils/effort.ts, src/utils/thinking.ts |
priority chain (:152-168), adaptive vs budget (:10-13) |
| Auth and token | src/services/oauth/index.ts, src/utils/auth.ts, src/utils/secureStorage/ |
PKCE (:14-120), locked refresh (:1427-1560), Keychain |
| Model and fallback | src/utils/model/model.ts, src/services/api/withRetry.ts |
priority chain (:84-101), FallbackTriggeredError (:163-165) |
| Permission modes | src/utils/permissions/PermissionMode.ts, getNextPermissionMode.ts |
six-grade semantics (:42-91), Shift+Tab cycle (:34-79) |
| Output styles | src/constants/outputStyles.ts, outputStyles/loadOutputStylesDir.ts |
built-in styles (:42-100), injection location (prompts.ts:152-157) |
| Memory system | src/memdir/memdir.ts, src/services/SessionMemory/sessionMemory.ts |
MEMORY.md cap (:35-95), session notes |
| Feature quick look | src/vim/, src/voice/, src/bridge/, src/cost-tracker.ts, src/cli/print.ts |
entries for vim/voice/remote/cost/SDK |
A note about this source tree: this is a TypeScript snapshot of an internal build (restored via sourcemap). Function names are mostly preserved; comments are thorough. You may see compile-time switches like
feature('XXX')and a few missing files (e.g. some bash-permission modules are merged in this snapshot); those are normal traces of the build system and do not block understanding the main flow.
Takeaway: three sentences to leave with
- The model can only talk; the agent software is its hands and eyes — Claude Code's entire job is "translate the model's intent into operations, translate the results of those operations back into text."
- The heart is a
whileloop: request the model → if there is atool_use, execute it and append the result → request again → until the model no longer wants a tool, and the answer is out. Compaction, streaming, sub-agents, interrupt — all of them are supporting facilities around this loop. - Safety is architecture, not virtue: the dead rules of input routing, the five-gate permission adjudication, the hooks snapshot, the ESC brake that goes all the way to the network cable — these are hard constraints at the software layer. Whatever the model wants to do, it has to "apply for approval," and the approval sits with you.
Once you have those three, look at any AI agent product on the market (Cursor, Devin, various automation assistants) and you will find they do not escape this basic frame — the differences are how rich the toolbox is, how fine-grained the loop is, and how strict the safety mechanisms are.
After you finish: where to practice next
After the first eleven chapters (the mechanism spine + the four field guides of tools / commands / ecosystem / features), the best consolidation is to use your hands. Easiest to hardest:
- Write your own slash command (section 9.4): drop a
.mdinto.claude/commands/, use$ARGUMENTSand!`shell`pre-execution; in ten minutes you have your first "private shortcut"; - Upgrade it to a skill (section 10.2): move it into
.claude/skills/<name>/SKILL.md, writewhen_to_useandallowed-tools, and feel how "progressive disclosure" lets the model find it itself; - Install an MCP server (section 10.3): register one in the project's
.mcp.json(e.g. the official GitHub server), walk the startup approval, then look at themcp__*tools that appeared in the tool list — you will feel "USB-C" plug-and-play for real; - Walk the official plugin marketplace (section 10.1):
/plugin marketplace add anthropics/claude-plugins-official, install a plugin, then go back and read itsplugin.jsonand directory structure, and see how other people package a workflow; - Run
/initonce and review the product line by line (section 9.3): have it generate a CLAUDE.md for your project, then edit it yourself against the standard "if we deleted this line, would Claude make a mistake?" — the fastest way to put chapters 5 and 11 to work.
Do these five things and you have not only "read" how Claude Code works; you are using it in the philosophy it was designed with.