1 · 入口层与启动流程

这一章讲:你在终端敲下 claude 到界面出现之间,程序做了什么。

1.1 四种启动形态

同一个可执行文件,根据参数进入四种完全不同的运行模式:

形态怎么触发用途与特征
交互模式
REPL
claude
(不带任何参数)
启动一个持续对话的终端界面。有输入框、有滚动的消息列表、有快捷键。这是绝大多数人使用的形态。
REPL = Read-Eval-Print Loop,「读取-求值-打印 循环」,是交互式命令行界面的通用叫法。
无头模式
headless / print
claude -p "帮我改这个文件" 不显示界面。给一个问题、执行完、把结果打印到标准输出、退出。用于写在脚本里自动化调用。
配合 --output-format json 可以输出机器可读的结构化结果。
软件开发工具包模式
Agent SDK
被别的程序作为库调用 输入输出都走标准输入输出的流式 JSON 协议(--input-format stream-json)。让其他软件可以把 Claude Code 当成一个智能体引擎嵌进去。
特殊子进程 --daemon-worker
--claude-in-chrome-mcp
由主进程派生的辅助进程。比如浏览器扩展的本地宿主、后台守护工作进程、远程桥接服务。

1.2 启动的第一个设计:快路径分派

entrypoints/cli.tsx 是真正的程序入口,只有 302 行。它的注释开门见山:

「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.」

译:引导入口 —— 在加载完整命令行程序之前先检查特殊参数。所有导入都是动态的,以便让快路径尽可能少地执行模块代码。--version 这条快路径除了本文件之外零导入。

为什么这很重要

先解释一个背景概念:JavaScript 程序在「导入」一个模块时,那个模块的顶层代码会立刻执行。如果一个程序静态导入了几百个模块,那么光是启动就要把这几百个模块全部执行一遍,即使这次运行根本用不到它们。

Claude Code 打包后是一个 300 MB 的单文件程序,模块数量极其庞大。如果每次运行都全量加载,claude --version 这种只想看一眼版本号的命令也要等好几秒。

所以入口文件用的是动态导入 —— 只有真的走到某条分支时,才去加载那条分支需要的模块:

async function main(): Promise<void> {
  const args = process.argv.slice(2);          // 取命令行参数

  // 快路径 1:--version,零模块加载
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);   // 版本号在编译期就被写死进来了
    return;                                     // 直接返回,什么都没加载
  }

  // 其余路径才加载启动性能分析器
  const { profileCheckpoint } = await import('../utils/startupProfiler.js');
  profileCheckpoint('cli_entry');               // 打一个时间戳

  // 快路径 2:--dump-system-prompt(导出系统提示词,用于评测)
  if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
    const { enableConfigs } = await import('../utils/config.js');
    ...
    return;
  }

  // 快路径 3:浏览器扩展的本地宿主进程
  if (process.argv[2] === '--claude-in-chrome-mcp') { ... return; }

  // 快路径 4:守护工作进程(由主进程派生,对性能敏感)
  if (feature('DAEMON') && args[0] === '--daemon-worker') {
    const { runDaemonWorker } = await import('../daemon/workerRegistry.js');
    await runDaemonWorker(args[1]);
    return;
  }
  ...
  // 全部快路径都不匹配 → 加载完整的命令行程序
}

claude-code/src/entrypoints/cli.tsx

MACRO.VERSION 里的 MACRO编译期宏 —— 打包时被替换成字面量字符串。所以拿版本号连读配置文件都不需要。

可以搬走的做法

如果你的命令行程序有「启动很慢」的问题,先看有没有高频的轻量命令被重量级的启动流程拖累了

典型的例子:--version--help、shell 补全脚本(这个尤其重要 —— 用户每敲一次 Tab 键就会调用一次)、以及被主进程高频派生的子进程。

把这些做成「在加载任何东西之前就分派掉」的快路径,收益立竿见影。

1.3 命令行参数:60 多个选项

完整的命令行接口定义在 main.tsx 里,用的是 Commander.js 这个库。选项数量非常多,下面按用途分组梳理:

模式与输入输出

选项作用
-p, --print无头模式:输出结果后退出。注意:这个模式会跳过「工作目录信任」确认对话框,所以只应在你信任的目录里用。
--output-format <格式>text(默认)/ json(单个结果对象)/ stream-json(实时流式)
--input-format <格式>text(默认)/ stream-json(从标准输入实时读取)
--json-schema <模式>要求输出符合指定的 JSON 结构。程序会用一个特殊的「结构化输出工具」强制模型产出合规结果,不合规就重试(最多 5 次)。
--include-partial-messages把模型流式返回的每一个片段都吐出来,而不只是完整消息

权限与安全

选项作用
--permission-mode <模式>设定权限模式。可选值见第 7 章。
--dangerously-skip-permissions跳过所有权限确认。官方描述:「仅推荐在没有互联网访问的沙箱环境中使用」。注意即使开了它,仍有一层检查绕不过 —— 第 7 章详述。
--allow-dangerously-skip-permissions只是允许使用上面那个模式,但不默认开启。给管理员做策略配置用。
--allowed-tools / --disallowed-tools允许 / 禁止的工具清单。支持带参数的写法,比如 Bash(git:*) 表示「只允许 git 开头的 bash 命令」。
--tools直接指定可用的内建工具集合。传空字符串就是禁用所有工具。
--add-dir <目录...>额外授权访问的目录(默认只能访问当前工作目录)

模型与预算

选项作用
--model <模型>可以传别名(sonnetopus)或完整型号名
--fallback-model <模型>主模型过载时自动降级到这个。只在无头模式下生效(交互模式下会直接问用户)
--effort <级别>思考力度:low / medium / high / max
--thinking <模式>enabled(等同 adaptive 自适应)/ disabled
--max-turns <次数>最多进行几轮。超过就提前退出。只在无头模式生效。
--max-budget-usd <金额>花费上限(美元)。超过就停。只在无头模式生效。

会话与恢复

选项作用
-c, --continue继续当前目录下最近的那次对话
-r, --resume [值]按会话 ID 恢复,或打开一个交互式选择器
--fork-session恢复时创建新的会话 ID,而不是复用原来的。相当于「从这个存档点分叉出一条新线」
--resume-session-at <消息 ID>只恢复到指定消息为止,后面的丢弃
--rewind-files <消息 ID>把文件恢复到某条消息时的状态然后退出。这是一个「撤销」功能 —— 第 11 章会讲文件历史怎么实现的。
--no-session-persistence不落盘。这次对话结束就没了,无法恢复。

扩展与集成

选项作用
--mcp-config <配置...>加载 MCP 外部工具服务(可以传文件路径或 JSON 字符串)
--strict-mcp-config只用命令行指定的 MCP 服务,忽略所有其他来源的配置
--plugin-dir <路径>从指定目录加载插件(可以重复传多个)
--agents <JSON>用 JSON 直接定义自定义子智能体
--settings <文件或 JSON>额外的配置来源
--setting-sources <来源>指定从哪几个来源读配置:user(用户级)/ project(项目级)/ local(本地覆盖)
--ide启动时自动连接编程软件(如果恰好只有一个可连的)
-w, --worktree [名字]为这次会话创建一个新的 git 工作树(隔离的代码副本)

1.4 --bare:一个值得单独讲的极简模式

这个选项的官方描述很长,值得逐条拆开看,因为它等于列出了「一次正常启动到底做了多少额外的事」

「Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery.」

译:极简模式:跳过钩子、语言服务协议、插件同步、提交署名、自动记忆、后台预取、系统钥匙串读取,以及 CLAUDE.md 的自动发现。

反过来读这句话,正常启动时会做这些事:

正常启动会做的事为什么它慢 / 有副作用
执行钩子用户配置的启动脚本,可能是任意程序,耗时不可控
启动语言服务协议
LSP
为了让模型能做「跳转到定义」这类代码导航,需要启动一个语言服务器进程 —— 这在大项目上可能要几秒
同步插件可能触发网络请求去拉取插件的最新版本
提交署名往 git 提交里追加署名信息,需要读 git 配置
自动记忆加载长期记忆目录
后台预取提前拉取可能用得上的数据
读系统钥匙串在 macOS 上读钥匙串会弹出系统授权对话框,在自动化脚本里是致命的
自动发现 CLAUDE.md沿着目录树向上逐级查找项目规范文件

--bare 模式还有一个关键的行为改变,描述里写得很明确:

「Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read).」

译:认证严格限定为环境变量 ANTHROPIC_API_KEY 或通过 --settings 指定的密钥获取脚本(OAuth 登录态和系统钥匙串永远不会被读取)。

这是为自动化场景专门设计的:认证来源必须是完全确定、不需要任何交互的。一个跑在持续集成流水线里的任务,绝不能因为「弹出了一个钥匙串授权框」而卡死。

1.5 启动时序

走完快路径分派之后,完整启动大致是这个顺序:

① 进程级环境准备 · 关闭 corepack 自动固定版本(一个会污染用户 package.json 的行为) · 如果是远程环境,把 Node 堆内存上限提到 8 GB · 特性开关的消融基线(内部实验用) ↓ ② 快路径分派(见 1.2) ↓ ③ enableConfigs() 加载配置 配置有多个来源,按优先级合并: 命令行参数 > 项目本地配置 > 项目配置 > 用户全局配置 ↓ ④ 认证解析 优先级:--settings 指定的密钥脚本 → 环境变量 → OAuth 登录态 → 系统钥匙串 ↓ ⑤ 并行加载三类扩展(互不依赖,同时进行) ├─ 技能(扫描技能目录,只读每个文件的头部元数据) ├─ 插件(从多个来源发现并加载) └─ MCP 外部工具服务(建立连接,拉取工具清单) ↓ ⑥ 组装工具清单 内建工具 → 按权限规则过滤 → 和 MCP 工具合并(分区排序,见第 4 章) ↓ ⑦ 构造系统提示词 fetchSystemPromptParts() 返回三部分: · defaultSystemPrompt 默认系统提示词 · userContext 用户上下文(拼在消息前面) · systemContext 系统上下文(拼在系统提示词后面) ↓ ⑧ 分支: ├─ 无头模式 → 直接创建 QueryEngine,跑一轮,输出,退出 └─ 交互模式 → 启动 React Ink 渲染循环,进入 REPL 主界面

1.6 系统提示词的三段结构

第 ⑦ 步返回的三个部分不是随意划分的,它们对应三个不同的缓存稳定性等级

部分放在哪里稳定性
defaultSystemPrompt
默认系统提示词
上下文最开头 最稳定。同一个版本的程序、同一个模型,所有用户都一样 —— 所以它能跨用户共享缓存
systemContext
系统上下文
追加在系统提示词之后 较稳定。包含当前工作目录、操作系统、git 状态等环境信息。同一个用户的同一场会话内基本不变。
userContext
用户上下文
拼在消息序列前面 最不稳定。包含用户的项目规范文件内容等。它被放在消息区而不是系统提示词区,正是为了不污染前面两段的缓存

对应的代码在主循环里是这样调用的:

// query.ts
const fullSystemPrompt = asSystemPrompt(
  appendSystemContext(systemPrompt, systemContext)     // 系统提示词 + 系统上下文
)
...
messages: prependUserContext(messagesForQuery, userContext)   // 用户上下文 + 消息
这个三段划分的价值

把「所有人都一样的部分」「这个用户不变的部分」「随时可能变的部分」按稳定性从高到低依次排列,是缓存友好的通用做法。

因为提示词缓存是前缀匹配的 —— 从最开头逐字比对,一旦对不上,后面全部失效。所以最不容易变的东西必须放在最前面。

如果把用户的项目规范文件(随时可能被编辑)放在系统提示词开头,那么用户每改一次 CLAUDE.md,整段缓存就全废。放到消息区之后,改动只影响它自己那一小段。

1 · The Entry Layer and Startup

This chapter covers what the program does between the moment you type claude in the terminal and the moment the interface appears.

1.1 Four launch modes

The same executable enters one of four completely different run modes depending on its arguments:

ModeHow to trigger itPurpose and characteristics
Interactive mode
REPL
claude
(no arguments)
Starts a terminal UI for an ongoing conversation. There is an input box, a scrolling message list, keyboard shortcuts. This is how the vast majority of people use it.
REPL = Read-Eval-Print Loop, the generic name for an interactive command-line interface.
Headless mode
headless / print
claude -p "fix this file for me" No interface. Give it a question, it runs to completion, prints the result to standard output, and exits. Meant for automated calls from scripts.
Combined with --output-format json it emits machine-readable structured output.
SDK mode
Agent SDK
Called as a library by another program Input and output both go over a streaming JSON protocol on standard input/output (--input-format stream-json). Lets other software embed Claude Code as an agent engine.
Special child processes --daemon-worker
--claude-in-chrome-mcp etc.
Helper processes spawned by the main process. For example the local host for the browser extension, background daemon workers, the remote bridge service.

1.2 The first design decision at startup: fast-path dispatch

entrypoints/cli.tsx is the real program entry point, and it is only 302 lines. Its header comment gets straight to the point:

“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.”

In other words: the bootstrap entry checks for special flags before loading the full CLI. Every import is dynamic, so the fast paths execute as little module code as possible. The --version fast path imports nothing beyond this file.

Why this matters

First, a bit of background: when a JavaScript program “imports” a module, that module’s top-level code runs immediately. If a program statically imports several hundred modules, then merely starting up means executing all several hundred of them, even if this particular run never uses them.

Bundled, Claude Code is a 300 MB single-file program with an enormous number of modules. If every run loaded all of it, even claude --version — a command that just wants to glance at the version number — would take several seconds.

So the entry file uses dynamic imports — a branch’s modules are loaded only when execution actually reaches that branch:

async function main(): Promise<void> {
  const args = process.argv.slice(2);          // grab the command-line arguments

  // Fast path 1: --version, zero modules loaded
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);   // the version is baked in at compile time
    return;                                     // return immediately; nothing was loaded
  }

  // Only the remaining paths load the startup profiler
  const { profileCheckpoint } = await import('../utils/startupProfiler.js');
  profileCheckpoint('cli_entry');               // record a timestamp

  // Fast path 2: --dump-system-prompt (export the system prompt, for evals)
  if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
    const { enableConfigs } = await import('../utils/config.js');
    ...
    return;
  }

  // Fast path 3: the local host process for the browser extension
  if (process.argv[2] === '--claude-in-chrome-mcp') { ... return; }

  // Fast path 4: daemon worker (spawned by the main process; performance-sensitive)
  if (feature('DAEMON') && args[0] === '--daemon-worker') {
    const { runDaemonWorker } = await import('../daemon/workerRegistry.js');
    await runDaemonWorker(args[1]);
    return;
  }
  ...
  // No fast path matched → load the full CLI
}

claude-code/src/entrypoints/cli.tsx

The MACRO in MACRO.VERSION is a compile-time macro — it is replaced with a literal string at bundle time. So getting the version number does not even require reading a config file.

A practice you can take with you

If your command-line program has a “slow startup” problem, first check whether high-frequency lightweight commands are being dragged down by a heavyweight startup sequence.

Typical examples: --version, --help, shell completion scripts (these matter most — they run every time the user presses Tab), and child processes the main process spawns frequently.

Turn those into fast paths that are “dispatched before anything is loaded,” and the payoff is immediate.

1.3 Command-line arguments: 60-plus options

The full command-line interface is defined in main.tsx using the Commander.js library. There are a great many options; below they are grouped by purpose:

Mode and I/O

OptionEffect
-p, --printHeadless mode: print the result and exit. Note: this mode skips the “trust this working directory” confirmation dialog, so use it only in directories you trust.
--output-format <format>text (default) / json (a single result object) / stream-json (real-time streaming)
--input-format <format>text (default) / stream-json (read from standard input in real time)
--json-schema <schema>Require the output to conform to a given JSON structure. The program uses a special “structured output tool” to force the model to produce a conforming result, and retries if it does not (up to 5 times).
--include-partial-messagesEmit every fragment of the model’s streamed response, not just complete messages

Permissions and safety

OptionEffect
--permission-mode <mode>Set the permission mode. See Chapter 7 for the possible values.
--dangerously-skip-permissionsSkip every permission prompt. Official description: “only recommended for sandboxes with no internet access.” Note that even with this on, one layer of checks cannot be bypassed — Chapter 7 goes into detail.
--allow-dangerously-skip-permissionsOnly allows the mode above to be used, without turning it on by default. For administrators configuring policy.
--allowed-tools / --disallowed-toolsLists of allowed / forbidden tools. Supports argument patterns, e.g. Bash(git:*) means “only allow bash commands that start with git.”
--toolsDirectly specify the set of built-in tools available. Passing an empty string disables all tools.
--add-dir <dirs...>Additional directories to grant access to (by default only the current working directory is accessible)

Model and budget

OptionEffect
--model <model>Accepts an alias (sonnet, opus) or a full model name
--fallback-model <model>Automatically fall back to this when the primary model is overloaded. Only takes effect in headless mode (interactive mode asks the user directly)
--effort <level>Thinking effort: low / medium / high / max
--thinking <mode>enabled (same as adaptive) / disabled
--max-turns <count>Maximum number of turns. Exits early when exceeded. Headless mode only.
--max-budget-usd <amount>Spend cap (US dollars). Stops when exceeded. Headless mode only.

Sessions and resume

OptionEffect
-c, --continueContinue the most recent conversation in the current directory
-r, --resume [value]Resume by session ID, or open an interactive picker
--fork-sessionOn resume, create a new session ID instead of reusing the original. Equivalent to “branch a new line off this save point”
--resume-session-at <message ID>Resume only up to the given message; discard everything after it
--rewind-files <message ID>Restore files to their state as of a given message, then exit. This is an “undo” feature — Chapter 11 explains how file history is implemented.
--no-session-persistenceDo not write to disk. When this conversation ends it is gone and cannot be resumed.

Extensions and integrations

OptionEffect
--mcp-config <configs...>Load MCP external tool servers (accepts file paths or JSON strings)
--strict-mcp-configUse only the MCP servers given on the command line; ignore config from every other source
--plugin-dir <path>Load plugins from the given directory (can be repeated)
--agents <JSON>Define custom subagents directly in JSON
--settings <file or JSON>An additional config source
--setting-sources <sources>Which sources to read config from: user (user level) / project (project level) / local (local overrides)
--ideAutomatically connect to an IDE at startup (if exactly one is available)
-w, --worktree [name]Create a new git worktree (an isolated copy of the code) for this session

1.4 --bare: a minimal mode worth its own section

This option’s official description is long, and it is worth taking apart item by item, because it amounts to a list of “how much extra work a normal startup actually does”:

“Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery.”

In other words: minimal mode skips hooks, the Language Server Protocol, plugin sync, commit attribution, auto-memory, background prefetches, system keychain reads, and automatic discovery of CLAUDE.md.

Read in reverse, that sentence says a normal startup does all of the following:

What a normal startup doesWhy it is slow / has side effects
Run hooksUser-configured startup scripts; they can be arbitrary programs, so their runtime is unbounded
Start the Language Server Protocol
LSP
To let the model do code navigation like “go to definition,” a language server process has to be started — on a large project that can take seconds
Sync pluginsMay trigger network requests to fetch the latest plugin versions
Commit attributionAppends attribution to git commits, which requires reading git config
Auto-memoryLoads the long-term memory directory
Background prefetchesFetches data that might be needed ahead of time
Read the system keychainOn macOS, reading the keychain pops up a system authorization dialog, which is fatal inside an automation script
Auto-discover CLAUDE.mdWalks up the directory tree level by level looking for project instruction files

--bare mode has one more key behavior change, stated plainly in the description:

“Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read).”

In other words: authentication is strictly limited to the ANTHROPIC_API_KEY environment variable or a key-helper script specified via --settings (the OAuth login state and the system keychain are never read).

This is designed specifically for automation: the source of credentials must be fully deterministic and require zero interaction. A job running in a continuous-integration pipeline must never hang because “a keychain authorization dialog popped up.”

1.5 Startup sequence

Once fast-path dispatch is out of the way, the full startup runs roughly in this order:

① Process-level environment setup · Disable corepack auto-pinning (a behavior that pollutes the user's package.json) · In a remote environment, raise the Node heap limit to 8 GB · Feature-flag ablation baseline (for internal experiments) ↓ ② Fast-path dispatch (see 1.2) ↓ ③ enableConfigs() load configuration Config comes from several sources, merged by priority: command-line args > project local config > project config > user global config ↓ ④ Resolve authentication Priority: key-helper script from --settings → environment variable → OAuth login state → system keychain ↓ ⑤ Load three kinds of extensions in parallel (independent of one another, run concurrently) ├─ Skills (scan the skill directories, reading only each file's header metadata) ├─ Plugins (discovered and loaded from multiple sources) └─ MCP external tool servers (establish connections, fetch tool lists) ↓ ⑥ Assemble the tool list Built-in tools → filter by permission rules → merge with MCP tools (partitioned ordering, see Chapter 4) ↓ ⑦ Build the system prompt fetchSystemPromptParts() returns three parts: · defaultSystemPrompt the default system prompt · userContext user context (prepended to the messages) · systemContext system context (appended after the system prompt) ↓ ⑧ Branch: ├─ Headless mode → create a QueryEngine directly, run one round, print, exit └─ Interactive mode → start the React Ink render loop, enter the REPL main screen

1.6 The three-part structure of the system prompt

The three parts returned in step ⑦ are not an arbitrary split; they correspond to three different levels of cache stability:

PartWhere it goesStability
defaultSystemPrompt
default system prompt
Very start of the context Most stable. Identical for every user on the same program version and the same model — so its cache can be shared across users.
systemContext
system context
Appended after the system prompt Fairly stable. Contains environment information such as the current working directory, operating system, git status. Essentially unchanged within one user’s single session.
userContext
user context
Prepended to the message sequence Least stable. Contains things like the contents of the user’s project instruction files. It lives in the message area rather than the system prompt area precisely so that it does not invalidate the cache for the two parts above.

The corresponding code in the main loop calls it like this:

// query.ts
const fullSystemPrompt = asSystemPrompt(
  appendSystemContext(systemPrompt, systemContext)     // system prompt + system context
)
...
messages: prependUserContext(messagesForQuery, userContext)   // user context + messages
Why this three-way split pays off

Ordering “the part everyone shares,” “the part that stays fixed for this user,” and “the part that can change at any moment” from most to least stable is the general recipe for being cache-friendly.

Prompt caching is prefix-matched — it compares character by character from the very beginning, and the moment there is a mismatch, everything after it is invalidated. So whatever changes least must come first.

If the user’s project instruction file (which could be edited at any time) sat at the top of the system prompt, then every edit to CLAUDE.md would throw away the entire cache. Placed in the message area instead, a change affects only its own small segment.