Claude Code init 命令深度源码解析:从输入到 CLAUDE.md 生成的完整链路
- 内容介绍
- 文章标签
- 相关推荐
前言
当我们首次在一个项目中使用 Claude Code 时,第一件事通常是输入 /init。这个命令会自动分析你的代码库,生成一个 CLAUDE.md 文件——它是 Claude Code 理解你项目的"记忆锚点",后续每次对话都会自动加载。
但 /init 到底做了什么?它只是一条简单的提示词,还是背后有复杂的工程机制?本文将沿着源码调用链,从用户输入 /init 的那一刻起,追踪到 CLAUDE.md 文件被写入磁盘、再到被加载进每次会话的完整过程。
一、全局视角:调用链总览
先给出完整的调用链路图,后面逐一拆解:
用户输入 "/init"
│
▼
processSlashCommand.tsx ──→ 匹配 Command 对象
│
▼
init.ts: getPromptForCommand() ──→ 返回 Prompt 文本
│
├── OLD_INIT_PROMPT (经典模式)
└── NEW_INIT_PROMPT (新版 8 阶段模式)
│
▼
Prompt 作为 user message 发送给 Claude API
│
▼
Claude 使用工具探索代码库(Read/Glob/Grep/AskUserQuestion...)
│
▼
Claude 调用 FileWriteTool 写入 CLAUDE.md
│
├── 记录 tengu_write_claudemd 分析事件
└── maybeMarkProjectOnboardingComplete() 标记引导完成
│
▼
下次会话启动时:
context.ts: getUserContext()
│
▼
claudemd.ts: getMemoryFiles() ──→ 多层级文件发现
│
▼
claudemd.ts: getClaudeMds() ──→ 拼接为系统提示注入 Claude
二、命令注册:/init 从哪里来?
2.1 命令定义
/init 是一个 type: 'prompt' 类型的命令,定义在 src/commands/init.ts:226-256:
// src/commands/init.ts:226-256
const command = {
type: 'prompt',
name: 'init',
get description() {
return feature('NEW_INIT') &&
(process.env.USER_TYPE === 'ant' ||
isEnvTruthy(process.env.CLAUDE_CODE_NEW_INIT))
? 'Initialize new CLAUDE.md file(s) and optional skills/hooks with codebase documentation'
: 'Initialize a new CLAUDE.md file with codebase documentation'
},
contentLength: 0,
progressMessage: 'analyzing your codebase',
source: 'builtin',
async getPromptForCommand() {
maybeMarkProjectOnboardingComplete()
return [
{
type: 'text',
text:
feature('NEW_INIT') &&
(process.env.USER_TYPE === 'ant' ||
isEnvTruthy(process.env.CLAUDE_CODE_NEW_INIT))
? NEW_INIT_PROMPT
: OLD_INIT_PROMPT,
},
]
},
} satisfies Command
几个关键点:
type: 'prompt'— 这是理解/init本质的关键。它不是一个本地执行命令(type: 'local'),而是一个提示词命令。它的getPromptForCommand返回的文本会被当作 user message 发送给 Claude APIcontentLength: 0— 表示命令本身不携带用户输入的额外内容(/init后面通常不跟参数)progressMessage: 'analyzing your codebase'— 用户在等待时看到的提示文字- Feature flag 分支 — 通过
feature('NEW_INIT')控制使用哪套 Prompt
2.2 命令注册
src/commands.ts 中完成了 init 命令的注册:
// src/commands.ts:25
import init from './commands/init.js'
// src/commands.ts:282(在 COMMANDS 数组中)
const COMMANDS = memoize((): Command[] => [
// ... 其他命令
init,
// ...
])
COMMANDS 数组用 memoize 包装——因为底层函数需要读取配置,不能在模块初始化阶段执行。所有命令(内置 + 技能 + 插件 + 工作流)最终通过 getCommands() 合并后对外暴露。
2.3 Command 类型系统
来看 PromptCommand 的类型定义(src/types/command.ts:25-57):
// src/types/command.ts:25-57
export type PromptCommand = {
type: 'prompt'
progressMessage: string
contentLength: number
argNames?: string[]
allowedTools?: string[]
model?: string
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
disableNonInteractive?: boolean
hooks?: HooksSettings
skillRoot?: string
context?: 'inline' | 'fork'
agent?: string
effort?: EffortValue
paths?: string[]
getPromptForCommand(
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}
getPromptForCommand 是核心方法 — 它返回 ContentBlockParam[],这是 Anthropic SDK 定义的消息内容块类型。返回的内容会被包装成 user message 发送给 Claude 模型。
三、Prompt 内容:两套策略
/init 的核心是它的 Prompt 文本。当前有两个版本:
3.1 经典模式(OLD_INIT_PROMPT)
src/commands/init.ts:6-26,仅 20 行,简单直接:
// src/commands/init.ts:6-26
const OLD_INIT_PROMPT = `Please analyze this codebase and create a CLAUDE.md file,
which will be given to future instances of Claude Code to operate in this repository.
What to add:
1. Commands that will be commonly used, such as how to build, lint, and run tests.
Include the necessary commands to develop in this codebase, such as how to run
a single test.
2. High-level code architecture and structure so that future instances can be
productive more quickly. Focus on the "big picture" architecture that requires
reading multiple files to understand.
Usage notes:
- If there's already a CLAUDE.md, suggest improvements to it.
- When you make the initial CLAUDE.md, do not repeat yourself and do not include
obvious instructions like "Provide helpful error messages to users"...
- Avoid listing every component or file structure that can be easily discovered.
- Don't include generic development practices.
- If there are Cursor rules or Copilot rules, make sure to include the important parts.
- Be sure to prefix the file with the following text:
...`
经典模式的特点:“给 Claude 一个目标,让它自己探索”。不做交互式引导,直接让 Claude 分析代码库并输出 CLAUDE.md。
3.2 新版 8 阶段模式(NEW_INIT_PROMPT)
src/commands/init.ts:28-224,约 200 行,是一个精心设计的多阶段交互式流程:
Phase 1: Ask what to set up → 询问用户要创建哪些文件
Phase 2: Explore the codebase → 子 Agent 探索代码库
Phase 3: Fill in the gaps → 交互式问答补齐信息
Phase 4: Write CLAUDE.md → 写入项目级配置
Phase 5: Write CLAUDE.local.md → 写入个人级配置
Phase 6: Suggest and create skills → 创建技能文件
Phase 7: Suggest additional opts → 额外优化建议
Phase 8: Summary and next steps → 总结与后续步骤
让我们逐阶段拆解关键设计。
Phase 1:用户选择
// src/commands/init.ts:32-42
Use AskUserQuestion to find out what the user wants:
- "Which CLAUDE.md files should /init set up?"
Options: "Project CLAUDE.md" | "Personal CLAUDE.local.md" | "Both project + personal"
Description for project: "Team-shared instructions checked into source control..."
Description for personal: "Your private preferences for this project (gitignored)..."
- "Also set up skills and hooks?"
Options: "Skills + hooks" | "Skills only" | "Hooks only" | "Neither, just CLAUDE.md"
这里通过 AskUserQuestion 工具实现交互——这不是硬编码的 UI,而是让 Claude 模型调用 AskUserQuestion 工具来呈现选项。用户的选择决定了后续阶段的执行范围。
Phase 2:代码库探索
// src/commands/init.ts:46-57
Launch a subagent to survey the codebase, and ask it to read key files to understand
the project: manifest files (package.json, Cargo.toml, pyproject.toml, go.mod, pom.xml,
etc.), README, Makefile/build configs, CI config, existing CLAUDE.md, .claude/rules/,
AGENTS.md, .cursor/rules or .cursorrules, .github/copilot-instructions.md,
.windsurfrules, .clinerules, .mcp.json.
Detect:
- Build, test, and lint commands (especially non-standard ones)
- Languages, frameworks, and package manager
- Project structure (monorepo with workspaces, multi-module, or single project)
- Code style rules that differ from language defaults
- Formatter configuration (prettier, biome, ruff, black, gofmt, rustfmt...)
- Git worktree usage: run `git worktree list` to check
注意 “Launch a subagent” — 新版 /init 不是让 Claude 本身去一个个文件读,而是指示 Claude 使用 AgentTool 派发一个子 Agent 去并行探索。这大大加快了初始化速度。
探索的范围非常全面:不仅包括本项目的配置(package.json、CI 配置),还会检查其他 AI 编码工具的规则文件(Cursor Rules、Copilot Instructions、Windsurf Rules、Cline Rules),从中提取有价值的信息。
Phase 3:交互式补齐
// src/commands/init.ts:60-93
Use AskUserQuestion to gather what you still need to write good CLAUDE.md files
and skills. Ask only things the code can't answer.
If the user chose project CLAUDE.md or both: ask about codebase practices —
non-obvious commands, gotchas, branch/PR conventions...
If the user chose personal CLAUDE.local.md or both: ask about them, not the codebase.
Examples:
- What's their role on the team?
- How familiar are they with this codebase and its languages/frameworks?
- Do they have personal sandbox URLs, test accounts, API key paths?
这个阶段的设计理念是 “只问代码回答不了的问题”。Phase 2 能从代码中推断出的(语言、框架、构建命令),不再重复询问。只针对非技术性的上下文(团队惯例、个人偏好)进行补充。
更有趣的是 proposal 展示机制(第 81-93 行):
// src/commands/init.ts:81-93
Show the proposal via AskUserQuestion's `preview` field, not as a separate text message
— the dialog overlays your output, so preceding text is hidden.
Keep previews compact — the preview box truncates with no scrolling. Example:
• **Format-on-edit hook** (automatic) — `ruff format <file>` via PostToolUse
• **/verify skill** (on-demand) — `make lint && make typecheck && make test`
• **CLAUDE.md note** (guideline) — "run lint/typecheck/test before marking done"
Prompt 甚至规定了 UI 交互细节——利用 AskUserQuestion 工具的 preview 字段做方案展示,让用户在审批时能看到完整的 diff 预览。
Phase 4:写入 CLAUDE.md
Phase 4 的写入规则(src/commands/init.ts:96-136)体现了一个重要的设计哲学:
// src/commands/init.ts:97
Write a minimal CLAUDE.md at the project root. Every line must pass this test:
"Would removing this cause Claude to make mistakes?" If no, cut it.
极简原则——CLAUDE.md 被加载进每次会话,所以体积直接影响性能。Prompt 明确列出了 Include 和 Exclude 清单:
Include(写入):
- 构建/测试/lint 命令(非标准的)
- 与语言默认不同的代码风格规则
- 测试怪癖和指令
- 仓库礼仪(分支命名、PR 约定、提交风格)
- 必需的环境变量
- 非显而易见的架构决策
Exclude(不写):
- 文件级结构或组件列表(Claude 可以自己读)
- 语言标准约定(Claude 已知)
- 泛泛的建议(“写干净的代码”)
- 从 manifest 文件一看便知的命令(
npm test、cargo test)
还有一条关键的安全机制:
// src/commands/init.ts:131
If CLAUDE.md already exists: read it, propose specific changes as diffs,
and explain why each change improves it. Do not silently overwrite.
已有文件保护 — 不静默覆盖,而是读取、对比、以 diff 形式提议修改。
Phase 5-7:CLAUDE.local.md + Skills + Hooks
Phase 5 处理个人配置文件 CLAUDE.local.md(src/commands/init.ts:137-153),有一个巧妙的 Git worktree 处理:
// src/commands/init.ts:150
If Phase 2 found multiple git worktrees and the user confirmed they use
sibling/external worktrees: Write the actual personal content to
~/.claude/<project-name>-instructions.md and make CLAUDE.local.md a one-line
stub that imports it: @~/.claude/<project-name>-instructions.md
当用户使用外部 worktree 时,个人配置写入全局目录,各 worktree 用 @import 引用——避免同一份配置在多个 worktree 间重复维护。
Phase 6 创建技能文件(src/commands/init.ts:154-181),支持按需触发的工作流(验证、部署、会话报告等)。
Phase 7 最有趣的是 Hook 创建流程(src/commands/init.ts:196-209):
// src/commands/init.ts:207
Load the hook reference (once per /init run, before the first hook):
invoke the Skill tool with skill: 'update-config' and args starting with
[hooks-only] followed by a one-line summary — e.g.,
[hooks-only] Constructing a PostToolUse/Write|Edit format hook using ruff
/init 会调用另一个 Skill(update-config)来辅助构建 Hook——这是 Skill 间的组合调用,让 /init 不需要自己理解 Hook 的 JSON Schema,而是委托专门的配置技能来处理。
四、命令执行管道:从文本到 API 调用
4.1 Slash Command 处理
当用户输入 /init 后,处理链路进入 processSlashCommand.tsx。对于 type: 'prompt' 的命令,核心调用在 getMessagesForPromptSlashCommand 函数中(约第 869 行):
// src/utils/processUserInput/processSlashCommand.tsx:869
const result = await command.getPromptForCommand(args, context);
返回的 ContentBlockParam[] 被包装成 user message,然后走正常的 Claude API 调用流程。
4.2 Coordinator Mode 下的特殊处理
如果当前处于 Coordinator Mode,/init 的处理完全不同(processSlashCommand.tsx:827-868):
// src/utils/processUserInput/processSlashCommand.tsx:837-862
if (feature('COORDINATOR_MODE') && isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
&& !context.agentId) {
const parts: string[] = [
`Skill "/${command.name}" is available for workers.`
];
if (command.description) {
parts.push(`Description: ${command.description}`);
}
if (command.whenToUse) {
parts.push(`When to use: ${command.whenToUse}`);
}
// ... 返回摘要信息而非完整 prompt
}
编排者不会执行 /init 的完整 prompt,而是看到一条摘要——告诉它"这个技能可以委派给 Worker"。注意 !context.agentId 的判断:当 Worker 自己调用该 Skill 时(有 agentId),才会走完整的 getPromptForCommand 路径。
五、文件写入:CLAUDE.md 落盘
Claude 在分析完代码库后,会调用 FileWriteTool 创建 CLAUDE.md。在 src/tools/FileWriteTool/FileWriteTool.ts:339-342 中有一个专门的埋点:
// src/tools/FileWriteTool/FileWriteTool.ts:339-342
// Log when writing to CLAUDE.md
if (fullFilePath.endsWith(`${sep}CLAUDE.md`)) {
logEvent('tengu_write_claudemd', {})
}
每次写入 CLAUDE.md 都会触发 tengu_write_claudemd 分析事件——这让 Anthropic 能追踪 /init 的实际使用情况。
六、项目引导状态追踪
6.1 Onboarding 步骤
src/projectOnboardingState.ts:19-41 定义了项目引导的步骤:
// src/projectOnboardingState.ts:19-41
export function getSteps(): Step[] {
const hasClaudeMd = getFsImplementation().existsSync(
join(getCwd(), 'CLAUDE.md'),
)
const isWorkspaceDirEmpty = isDirEmpty(getCwd())
return [
{
key: 'workspace',
text: 'Ask Claude to create a new app or clone a repository',
isComplete: false,
isCompletable: true,
isEnabled: isWorkspaceDirEmpty,
},
{
key: 'claudemd',
text: 'Run /init to create a CLAUDE.md file with instructions for Claude',
isComplete: hasClaudeMd,
isCompletable: true,
isEnabled: !isWorkspaceDirEmpty,
},
]
}
两个步骤的启用条件互斥:
- 空目录 → 提示"创建新项目或克隆仓库"
- 非空目录 → 提示"运行 /init 创建 CLAUDE.md"
引导完成的检测逻辑——只要 CLAUDE.md 文件存在(existsSync),claudemd 步骤就标记为完成。
6.2 完成标记
/init 的 getPromptForCommand 开头就调用了 maybeMarkProjectOnboardingComplete()(src/commands/init.ts:240):
// src/projectOnboardingState.ts:49-61
export function maybeMarkProjectOnboardingComplete(): void {
if (getCurrentProjectConfig().hasCompletedProjectOnboarding) {
return // 缓存短路:已完成则直接返回
}
if (isProjectOnboardingComplete()) {
saveCurrentProjectConfig(current => ({
...current,
hasCompletedProjectOnboarding: true,
}))
}
}
注意两层检查:
- 先检查配置缓存(避免每次都读文件系统)
- 再检查实际完成状态
一旦标记完成,后续 REPL 的每次 prompt 提交都不会再触发文件系统检查。
七、CLAUDE.md 的发现与加载
/init 创建的 CLAUDE.md 在下一次(或同一次)会话中被自动加载。这一机制由 src/utils/claudemd.ts 和 src/context.ts 协作完成。
7.1 入口:getUserContext
src/context.ts:155-188 是上下文注入的入口:
// src/context.ts:155-188
export const getUserContext = memoize(
async (): Promise<{ [k: string]: string }> => {
const shouldDisableClaudeMd =
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
setCachedClaudeMdContent(claudeMd || null)
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}
},
)
调用链:getUserContext → getMemoryFiles() → getClaudeMds() → 注入系统提示。
7.2 多层级文件发现:getMemoryFiles
这是最复杂的部分。src/utils/claudemd.ts:790-934 的 getMemoryFiles 实现了一套多层级、多类型、向上遍历的文件发现机制:
// src/utils/claudemd.ts:790-934
export const getMemoryFiles = memoize(
async (forceIncludeExternal: boolean = false): Promise<MemoryFileInfo[]> => {
const result: MemoryFileInfo[] = []
const processedPaths = new Set<string>()
// 第一层:Managed(全局策略,始终加载)
const managedClaudeMd = getMemoryPath('Managed')
result.push(
...(await processMemoryFile(managedClaudeMd, 'Managed', processedPaths, includeExternal)),
)
// 第二层:User(用户全局配置)
if (isSettingSourceEnabled('userSettings')) {
const userClaudeMd = getMemoryPath('User')
result.push(
...(await processMemoryFile(userClaudeMd, 'User', processedPaths, true)),
)
}
// 第三层:Project + Local(项目级 + 个人级)
// 从当前目录向上遍历到根目录
let currentDir = originalCwd
while (currentDir !== parse(currentDir).root) {
dirs.push(currentDir)
currentDir = dirname(currentDir)
}
// 从根目录向下处理到当前目录
for (const dir of dirs.reverse()) {
// CLAUDE.md(Project 类型)
const projectPath = join(dir, 'CLAUDE.md')
result.push(
...(await processMemoryFile(projectPath, 'Project', processedPaths, includeExternal)),
)
// .claude/CLAUDE.md(Project 类型)
const dotClaudePath = join(dir, '.claude', 'CLAUDE.md')
result.push(
...(await processMemoryFile(dotClaudePath, 'Project', processedPaths, includeExternal)),
)
// .claude/rules/*.md(Project 类型)
const rulesDir = join(dir, '.claude', 'rules')
result.push(
...(await processMdRules({ rulesDir, type: 'Project', ... })),
)
// CLAUDE.local.md(Local 类型)
const localPath = join(dir, 'CLAUDE.local.md')
result.push(
...(await processMemoryFile(localPath, 'Local', processedPaths, includeExternal)),
)
}
},
)
发现顺序(优先级从高到低):
| 层级 | 路径 | 类型 | 用途 |
|---|---|---|---|
| 1 | /etc/claude-code/CLAUDE.md |
Managed | 全局策略(企业管理员设定) |
| 2 | ~/.claude/CLAUDE.md |
User | 用户全局偏好 |
| 3 | <project>/CLAUDE.md |
Project | 项目团队共享配置(/init 创建) |
| 3 | <project>/.claude/CLAUDE.md |
Project | 项目配置(替代位置) |
| 3 | <project>/.claude/rules/*.md |
Project | 按主题拆分的规则文件 |
| 4 | <project>/CLAUDE.local.md |
Local | 个人私有配置(gitignored) |
向上遍历的妙处
文件发现不是只看当前目录。它会从当前目录向上遍历到文件系统根,收集所有层级的 CLAUDE.md。这意味着 monorepo 可以在根目录放通用规则,各子项目放专用规则:
my-monorepo/
├── CLAUDE.md ← 根级通用规则
├── packages/
│ ├── frontend/
│ │ └── CLAUDE.md ← 前端专用规则
│ └── backend/
│ └── CLAUDE.md ← 后端专用规则
当 Claude 在 packages/frontend/ 下工作时,会同时加载根级和前端专用的 CLAUDE.md。
Git Worktree 去重
有一段精巧的 worktree 处理逻辑(src/utils/claudemd.ts:859-884):
// src/utils/claudemd.ts:868-884
const gitRoot = findGitRoot(originalCwd)
const canonicalRoot = findCanonicalGitRoot(originalCwd)
const isNestedWorktree =
gitRoot !== null &&
canonicalRoot !== null &&
normalizePathForComparison(gitRoot) !==
normalizePathForComparison(canonicalRoot) &&
pathInWorkingPath(gitRoot, canonicalRoot)
for (const dir of dirs.reverse()) {
const skipProject =
isNestedWorktree &&
pathInWorkingPath(dir, canonicalRoot) &&
!pathInWorkingPath(dir, gitRoot)
// ...
}
当在嵌套 worktree(如 .claude/worktrees/<name>/)中工作时,向上遍历会经过 worktree 根和主仓库根——两者都有 CLAUDE.md。这段代码检测到嵌套 worktree 后,跳过主仓库中的 Project 类型文件(因为 worktree 已有自己的副本),但不跳过 CLAUDE.local.md(因为它是 gitignored 的,只存在于主仓库)。
7.3 递归处理 @include
processMemoryFile(src/utils/claudemd.ts:618-685)不只是读取单个文件。它支持 @include 语法的递归展开:
// src/utils/claudemd.ts:618-685
export async function processMemoryFile(
filePath: string,
type: MemoryType,
processedPaths: Set<string>,
includeExternal: boolean,
depth: number = 0,
parent?: string,
): Promise<MemoryFileInfo[]> {
const normalizedPath = normalizePathForComparison(filePath)
if (processedPaths.has(normalizedPath) || depth >= MAX_INCLUDE_DEPTH) {
return [] // 去重 + 深度限制,防止循环引用
}
if (isClaudeMdExcluded(filePath, type)) {
return [] // 支持排除规则
}
processedPaths.add(normalizedPath)
const { info: memoryFile, includePaths: resolvedIncludePaths } =
await safelyReadMemoryFileAsync(filePath, type, resolvedPath)
if (!memoryFile || !memoryFile.content.trim()) {
return [] // 文件不存在或为空
}
const result: MemoryFileInfo[] = []
result.push(memoryFile)
// 递归处理 @include 引用
for (const resolvedIncludePath of resolvedIncludePaths) {
const isExternal = !pathInOriginalCwd(resolvedIncludePath)
if (isExternal && !includeExternal) {
continue // 外部文件需要显式批准
}
const includedFiles = await processMemoryFile(
resolvedIncludePath, type, processedPaths,
includeExternal, depth + 1, filePath,
)
result.push(...includedFiles)
}
return result
}
关键的安全设计:
processedPaths去重 — 防止循环 @include 导致无限递归MAX_INCLUDE_DEPTH深度限制 — 兜底保护- 外部文件审批 — 项目外的 @include 需要用户显式批准(
hasClaudeMdExternalIncludesApproved)
7.4 拼接为系统提示
所有收集到的文件最终通过 getClaudeMds(src/utils/claudemd.ts:1153-1195)拼接:
// src/utils/claudemd.ts:1153-1195
export const getClaudeMds = (
memoryFiles: MemoryFileInfo[],
filter?: (type: MemoryType) => boolean,
): string => {
const memories: string[] = []
for (const file of memoryFiles) {
if (filter && !filter(file.type)) continue
if (file.content) {
const description =
file.type === 'Project'
? ' (project instructions, checked into the codebase)'
: file.type === 'Local'
? " (user's private project instructions, not checked in)"
: " (user's private global instructions for all projects)"
memories.push(`Contents of ${file.path}${description}:\n\n${content}`)
}
}
return `${MEMORY_INSTRUCTION_PROMPT}\n\n${memories.join('\n\n')}`
}
每个文件带上路径和类型标注,Claude 可以区分哪些是团队共享的、哪些是个人私有的——这影响 Claude 在修改这些文件时的行为(比如不会把个人偏好写入团队配置)。
八、/init-verifiers:配套的验证器初始化
除了 /init,还有一个配套命令 /init-verifiers(src/commands/init-verifiers.ts),它专门用于创建自动验证技能:
// src/commands/init-verifiers.ts:2-10
const command = {
type: 'prompt',
name: 'init-verifiers',
description: 'Create verifier skill(s) for automated verification of code changes',
progressMessage: 'analyzing your project and creating verifier skills',
// ...
}
它同样是 prompt 类型,通过 5 个阶段完成:
- Auto-Detection — 检测项目类型(Web/CLI/API)和已有的验证工具
- Verification Tool Setup — 安装缺失的工具(Playwright、MCP 等)
- Interactive Q&A — 确认验证器名称和项目细节
- Generate Verifier Skill — 写入
.claude/skills/<verifier-name>/SKILL.md - Confirm Creation — 告知用户创建结果
验证器按类型有不同的 allowed-tools(src/commands/init-verifiers.ts:212-245):
# Playwright 验证器
allowed-tools:
- Bash(npm:*)
- mcp__playwright__*
- Read, Glob, Grep
# CLI 验证器
allowed-tools:
- Tmux
- Bash(asciinema:*)
- Read, Glob, Grep
# API 验证器
allowed-tools:
- Bash(curl:*)
- Bash(http:*)
- Read, Glob, Grep
工具集按最小权限原则配置——每种验证器只有它需要的工具。
九、设计洞察
9.1 “Prompt as Code” 模式
/init 最核心的设计是:它不是一个程序,而是一段精心编写的提示词。200 行的 NEW_INIT_PROMPT 不包含任何 TypeScript 逻辑——纯文本指令。所有的"智能"(代码分析、交互引导、文件生成)都由 Claude 模型在运行时完成。
这意味着:
- 零代码的功能扩展 — 要改变
/init的行为,只需修改 Prompt 文本 - 天然的多语言支持 — 不需要为 Python/Rust/Go 项目写不同的分析逻辑
- 自适应能力 — Claude 模型的能力提升直接带来
/init质量提升
9.2 分层配置体系
CLAUDE.md 的多层级设计借鉴了 Git 的配置分层(system → global → local):
Managed (/etc/claude-code/) ← 企业管理员
↓
User (~/.claude/) ← 个人全局
↓
Project (CLAUDE.md) ← 团队共享
↓
Local (CLAUDE.local.md) ← 个人私有
每一层可以覆盖上一层,同时 @include 支持跨层引用。
9.3 安全边界
- 外部文件
@include需要显式批准 - 路径去重防止循环引用
- 深度限制防止栈溢出
- worktree 检测防止重复加载
- CLAUDE.local.md 自动 gitignore 保护隐私
十、文件索引
| 文件 | 核心职责 |
|---|---|
src/commands/init.ts |
/init 命令定义 + OLD/NEW 两套 Prompt |
src/commands/init-verifiers.ts |
/init-verifiers 验证器创建 |
src/commands.ts |
命令注册中心 |
src/types/command.ts |
Command/PromptCommand 类型定义 |
src/utils/processUserInput/processSlashCommand.tsx |
Slash 命令处理管道 |
src/tools/FileWriteTool/FileWriteTool.ts |
CLAUDE.md 写入 + 事件追踪 |
src/projectOnboardingState.ts |
引导状态追踪 |
src/utils/claudemd.ts |
CLAUDE.md 多层级发现 + @include 递归解析 |
src/context.ts |
上下文注入(getUserContext) |
总结
/init 的实现展现了 Claude Code 的一个核心设计哲学:用 Prompt 编程,而非用代码编程。
整个 /init 命令的 TypeScript 代码不到 30 行(命令定义 + feature flag 判断),但通过 200 行精心设计的 Prompt,实现了 8 个阶段的交互式初始化流程。真正的"业务逻辑"不在代码里,而在自然语言里。
而 CLAUDE.md 的发现与加载机制则是传统工程的精华:多层级遍历、去重、循环检测、安全边界、worktree 兼容——这些是需要确定性保证的基础设施,不能交给概率性的模型。
Prompt 负责智能决策,代码负责确定性基础设施——这也许是 AI-native 工具设计的一个普适模式。
网友解答:--【壹】--:
感谢佬友分享
干货长文,学习一下。
--【贰】--:
大佬辛苦了,感谢分析,claude code 有空学习一下
--【叁】--:
学习了,真不错。感谢大佬辛苦奉献!!!!
前言
当我们首次在一个项目中使用 Claude Code 时,第一件事通常是输入 /init。这个命令会自动分析你的代码库,生成一个 CLAUDE.md 文件——它是 Claude Code 理解你项目的"记忆锚点",后续每次对话都会自动加载。
但 /init 到底做了什么?它只是一条简单的提示词,还是背后有复杂的工程机制?本文将沿着源码调用链,从用户输入 /init 的那一刻起,追踪到 CLAUDE.md 文件被写入磁盘、再到被加载进每次会话的完整过程。
一、全局视角:调用链总览
先给出完整的调用链路图,后面逐一拆解:
用户输入 "/init"
│
▼
processSlashCommand.tsx ──→ 匹配 Command 对象
│
▼
init.ts: getPromptForCommand() ──→ 返回 Prompt 文本
│
├── OLD_INIT_PROMPT (经典模式)
└── NEW_INIT_PROMPT (新版 8 阶段模式)
│
▼
Prompt 作为 user message 发送给 Claude API
│
▼
Claude 使用工具探索代码库(Read/Glob/Grep/AskUserQuestion...)
│
▼
Claude 调用 FileWriteTool 写入 CLAUDE.md
│
├── 记录 tengu_write_claudemd 分析事件
└── maybeMarkProjectOnboardingComplete() 标记引导完成
│
▼
下次会话启动时:
context.ts: getUserContext()
│
▼
claudemd.ts: getMemoryFiles() ──→ 多层级文件发现
│
▼
claudemd.ts: getClaudeMds() ──→ 拼接为系统提示注入 Claude
二、命令注册:/init 从哪里来?
2.1 命令定义
/init 是一个 type: 'prompt' 类型的命令,定义在 src/commands/init.ts:226-256:
// src/commands/init.ts:226-256
const command = {
type: 'prompt',
name: 'init',
get description() {
return feature('NEW_INIT') &&
(process.env.USER_TYPE === 'ant' ||
isEnvTruthy(process.env.CLAUDE_CODE_NEW_INIT))
? 'Initialize new CLAUDE.md file(s) and optional skills/hooks with codebase documentation'
: 'Initialize a new CLAUDE.md file with codebase documentation'
},
contentLength: 0,
progressMessage: 'analyzing your codebase',
source: 'builtin',
async getPromptForCommand() {
maybeMarkProjectOnboardingComplete()
return [
{
type: 'text',
text:
feature('NEW_INIT') &&
(process.env.USER_TYPE === 'ant' ||
isEnvTruthy(process.env.CLAUDE_CODE_NEW_INIT))
? NEW_INIT_PROMPT
: OLD_INIT_PROMPT,
},
]
},
} satisfies Command
几个关键点:
type: 'prompt'— 这是理解/init本质的关键。它不是一个本地执行命令(type: 'local'),而是一个提示词命令。它的getPromptForCommand返回的文本会被当作 user message 发送给 Claude APIcontentLength: 0— 表示命令本身不携带用户输入的额外内容(/init后面通常不跟参数)progressMessage: 'analyzing your codebase'— 用户在等待时看到的提示文字- Feature flag 分支 — 通过
feature('NEW_INIT')控制使用哪套 Prompt
2.2 命令注册
src/commands.ts 中完成了 init 命令的注册:
// src/commands.ts:25
import init from './commands/init.js'
// src/commands.ts:282(在 COMMANDS 数组中)
const COMMANDS = memoize((): Command[] => [
// ... 其他命令
init,
// ...
])
COMMANDS 数组用 memoize 包装——因为底层函数需要读取配置,不能在模块初始化阶段执行。所有命令(内置 + 技能 + 插件 + 工作流)最终通过 getCommands() 合并后对外暴露。
2.3 Command 类型系统
来看 PromptCommand 的类型定义(src/types/command.ts:25-57):
// src/types/command.ts:25-57
export type PromptCommand = {
type: 'prompt'
progressMessage: string
contentLength: number
argNames?: string[]
allowedTools?: string[]
model?: string
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
disableNonInteractive?: boolean
hooks?: HooksSettings
skillRoot?: string
context?: 'inline' | 'fork'
agent?: string
effort?: EffortValue
paths?: string[]
getPromptForCommand(
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}
getPromptForCommand 是核心方法 — 它返回 ContentBlockParam[],这是 Anthropic SDK 定义的消息内容块类型。返回的内容会被包装成 user message 发送给 Claude 模型。
三、Prompt 内容:两套策略
/init 的核心是它的 Prompt 文本。当前有两个版本:
3.1 经典模式(OLD_INIT_PROMPT)
src/commands/init.ts:6-26,仅 20 行,简单直接:
// src/commands/init.ts:6-26
const OLD_INIT_PROMPT = `Please analyze this codebase and create a CLAUDE.md file,
which will be given to future instances of Claude Code to operate in this repository.
What to add:
1. Commands that will be commonly used, such as how to build, lint, and run tests.
Include the necessary commands to develop in this codebase, such as how to run
a single test.
2. High-level code architecture and structure so that future instances can be
productive more quickly. Focus on the "big picture" architecture that requires
reading multiple files to understand.
Usage notes:
- If there's already a CLAUDE.md, suggest improvements to it.
- When you make the initial CLAUDE.md, do not repeat yourself and do not include
obvious instructions like "Provide helpful error messages to users"...
- Avoid listing every component or file structure that can be easily discovered.
- Don't include generic development practices.
- If there are Cursor rules or Copilot rules, make sure to include the important parts.
- Be sure to prefix the file with the following text:
...`
经典模式的特点:“给 Claude 一个目标,让它自己探索”。不做交互式引导,直接让 Claude 分析代码库并输出 CLAUDE.md。
3.2 新版 8 阶段模式(NEW_INIT_PROMPT)
src/commands/init.ts:28-224,约 200 行,是一个精心设计的多阶段交互式流程:
Phase 1: Ask what to set up → 询问用户要创建哪些文件
Phase 2: Explore the codebase → 子 Agent 探索代码库
Phase 3: Fill in the gaps → 交互式问答补齐信息
Phase 4: Write CLAUDE.md → 写入项目级配置
Phase 5: Write CLAUDE.local.md → 写入个人级配置
Phase 6: Suggest and create skills → 创建技能文件
Phase 7: Suggest additional opts → 额外优化建议
Phase 8: Summary and next steps → 总结与后续步骤
让我们逐阶段拆解关键设计。
Phase 1:用户选择
// src/commands/init.ts:32-42
Use AskUserQuestion to find out what the user wants:
- "Which CLAUDE.md files should /init set up?"
Options: "Project CLAUDE.md" | "Personal CLAUDE.local.md" | "Both project + personal"
Description for project: "Team-shared instructions checked into source control..."
Description for personal: "Your private preferences for this project (gitignored)..."
- "Also set up skills and hooks?"
Options: "Skills + hooks" | "Skills only" | "Hooks only" | "Neither, just CLAUDE.md"
这里通过 AskUserQuestion 工具实现交互——这不是硬编码的 UI,而是让 Claude 模型调用 AskUserQuestion 工具来呈现选项。用户的选择决定了后续阶段的执行范围。
Phase 2:代码库探索
// src/commands/init.ts:46-57
Launch a subagent to survey the codebase, and ask it to read key files to understand
the project: manifest files (package.json, Cargo.toml, pyproject.toml, go.mod, pom.xml,
etc.), README, Makefile/build configs, CI config, existing CLAUDE.md, .claude/rules/,
AGENTS.md, .cursor/rules or .cursorrules, .github/copilot-instructions.md,
.windsurfrules, .clinerules, .mcp.json.
Detect:
- Build, test, and lint commands (especially non-standard ones)
- Languages, frameworks, and package manager
- Project structure (monorepo with workspaces, multi-module, or single project)
- Code style rules that differ from language defaults
- Formatter configuration (prettier, biome, ruff, black, gofmt, rustfmt...)
- Git worktree usage: run `git worktree list` to check
注意 “Launch a subagent” — 新版 /init 不是让 Claude 本身去一个个文件读,而是指示 Claude 使用 AgentTool 派发一个子 Agent 去并行探索。这大大加快了初始化速度。
探索的范围非常全面:不仅包括本项目的配置(package.json、CI 配置),还会检查其他 AI 编码工具的规则文件(Cursor Rules、Copilot Instructions、Windsurf Rules、Cline Rules),从中提取有价值的信息。
Phase 3:交互式补齐
// src/commands/init.ts:60-93
Use AskUserQuestion to gather what you still need to write good CLAUDE.md files
and skills. Ask only things the code can't answer.
If the user chose project CLAUDE.md or both: ask about codebase practices —
non-obvious commands, gotchas, branch/PR conventions...
If the user chose personal CLAUDE.local.md or both: ask about them, not the codebase.
Examples:
- What's their role on the team?
- How familiar are they with this codebase and its languages/frameworks?
- Do they have personal sandbox URLs, test accounts, API key paths?
这个阶段的设计理念是 “只问代码回答不了的问题”。Phase 2 能从代码中推断出的(语言、框架、构建命令),不再重复询问。只针对非技术性的上下文(团队惯例、个人偏好)进行补充。
更有趣的是 proposal 展示机制(第 81-93 行):
// src/commands/init.ts:81-93
Show the proposal via AskUserQuestion's `preview` field, not as a separate text message
— the dialog overlays your output, so preceding text is hidden.
Keep previews compact — the preview box truncates with no scrolling. Example:
• **Format-on-edit hook** (automatic) — `ruff format <file>` via PostToolUse
• **/verify skill** (on-demand) — `make lint && make typecheck && make test`
• **CLAUDE.md note** (guideline) — "run lint/typecheck/test before marking done"
Prompt 甚至规定了 UI 交互细节——利用 AskUserQuestion 工具的 preview 字段做方案展示,让用户在审批时能看到完整的 diff 预览。
Phase 4:写入 CLAUDE.md
Phase 4 的写入规则(src/commands/init.ts:96-136)体现了一个重要的设计哲学:
// src/commands/init.ts:97
Write a minimal CLAUDE.md at the project root. Every line must pass this test:
"Would removing this cause Claude to make mistakes?" If no, cut it.
极简原则——CLAUDE.md 被加载进每次会话,所以体积直接影响性能。Prompt 明确列出了 Include 和 Exclude 清单:
Include(写入):
- 构建/测试/lint 命令(非标准的)
- 与语言默认不同的代码风格规则
- 测试怪癖和指令
- 仓库礼仪(分支命名、PR 约定、提交风格)
- 必需的环境变量
- 非显而易见的架构决策
Exclude(不写):
- 文件级结构或组件列表(Claude 可以自己读)
- 语言标准约定(Claude 已知)
- 泛泛的建议(“写干净的代码”)
- 从 manifest 文件一看便知的命令(
npm test、cargo test)
还有一条关键的安全机制:
// src/commands/init.ts:131
If CLAUDE.md already exists: read it, propose specific changes as diffs,
and explain why each change improves it. Do not silently overwrite.
已有文件保护 — 不静默覆盖,而是读取、对比、以 diff 形式提议修改。
Phase 5-7:CLAUDE.local.md + Skills + Hooks
Phase 5 处理个人配置文件 CLAUDE.local.md(src/commands/init.ts:137-153),有一个巧妙的 Git worktree 处理:
// src/commands/init.ts:150
If Phase 2 found multiple git worktrees and the user confirmed they use
sibling/external worktrees: Write the actual personal content to
~/.claude/<project-name>-instructions.md and make CLAUDE.local.md a one-line
stub that imports it: @~/.claude/<project-name>-instructions.md
当用户使用外部 worktree 时,个人配置写入全局目录,各 worktree 用 @import 引用——避免同一份配置在多个 worktree 间重复维护。
Phase 6 创建技能文件(src/commands/init.ts:154-181),支持按需触发的工作流(验证、部署、会话报告等)。
Phase 7 最有趣的是 Hook 创建流程(src/commands/init.ts:196-209):
// src/commands/init.ts:207
Load the hook reference (once per /init run, before the first hook):
invoke the Skill tool with skill: 'update-config' and args starting with
[hooks-only] followed by a one-line summary — e.g.,
[hooks-only] Constructing a PostToolUse/Write|Edit format hook using ruff
/init 会调用另一个 Skill(update-config)来辅助构建 Hook——这是 Skill 间的组合调用,让 /init 不需要自己理解 Hook 的 JSON Schema,而是委托专门的配置技能来处理。
四、命令执行管道:从文本到 API 调用
4.1 Slash Command 处理
当用户输入 /init 后,处理链路进入 processSlashCommand.tsx。对于 type: 'prompt' 的命令,核心调用在 getMessagesForPromptSlashCommand 函数中(约第 869 行):
// src/utils/processUserInput/processSlashCommand.tsx:869
const result = await command.getPromptForCommand(args, context);
返回的 ContentBlockParam[] 被包装成 user message,然后走正常的 Claude API 调用流程。
4.2 Coordinator Mode 下的特殊处理
如果当前处于 Coordinator Mode,/init 的处理完全不同(processSlashCommand.tsx:827-868):
// src/utils/processUserInput/processSlashCommand.tsx:837-862
if (feature('COORDINATOR_MODE') && isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
&& !context.agentId) {
const parts: string[] = [
`Skill "/${command.name}" is available for workers.`
];
if (command.description) {
parts.push(`Description: ${command.description}`);
}
if (command.whenToUse) {
parts.push(`When to use: ${command.whenToUse}`);
}
// ... 返回摘要信息而非完整 prompt
}
编排者不会执行 /init 的完整 prompt,而是看到一条摘要——告诉它"这个技能可以委派给 Worker"。注意 !context.agentId 的判断:当 Worker 自己调用该 Skill 时(有 agentId),才会走完整的 getPromptForCommand 路径。
五、文件写入:CLAUDE.md 落盘
Claude 在分析完代码库后,会调用 FileWriteTool 创建 CLAUDE.md。在 src/tools/FileWriteTool/FileWriteTool.ts:339-342 中有一个专门的埋点:
// src/tools/FileWriteTool/FileWriteTool.ts:339-342
// Log when writing to CLAUDE.md
if (fullFilePath.endsWith(`${sep}CLAUDE.md`)) {
logEvent('tengu_write_claudemd', {})
}
每次写入 CLAUDE.md 都会触发 tengu_write_claudemd 分析事件——这让 Anthropic 能追踪 /init 的实际使用情况。
六、项目引导状态追踪
6.1 Onboarding 步骤
src/projectOnboardingState.ts:19-41 定义了项目引导的步骤:
// src/projectOnboardingState.ts:19-41
export function getSteps(): Step[] {
const hasClaudeMd = getFsImplementation().existsSync(
join(getCwd(), 'CLAUDE.md'),
)
const isWorkspaceDirEmpty = isDirEmpty(getCwd())
return [
{
key: 'workspace',
text: 'Ask Claude to create a new app or clone a repository',
isComplete: false,
isCompletable: true,
isEnabled: isWorkspaceDirEmpty,
},
{
key: 'claudemd',
text: 'Run /init to create a CLAUDE.md file with instructions for Claude',
isComplete: hasClaudeMd,
isCompletable: true,
isEnabled: !isWorkspaceDirEmpty,
},
]
}
两个步骤的启用条件互斥:
- 空目录 → 提示"创建新项目或克隆仓库"
- 非空目录 → 提示"运行 /init 创建 CLAUDE.md"
引导完成的检测逻辑——只要 CLAUDE.md 文件存在(existsSync),claudemd 步骤就标记为完成。
6.2 完成标记
/init 的 getPromptForCommand 开头就调用了 maybeMarkProjectOnboardingComplete()(src/commands/init.ts:240):
// src/projectOnboardingState.ts:49-61
export function maybeMarkProjectOnboardingComplete(): void {
if (getCurrentProjectConfig().hasCompletedProjectOnboarding) {
return // 缓存短路:已完成则直接返回
}
if (isProjectOnboardingComplete()) {
saveCurrentProjectConfig(current => ({
...current,
hasCompletedProjectOnboarding: true,
}))
}
}
注意两层检查:
- 先检查配置缓存(避免每次都读文件系统)
- 再检查实际完成状态
一旦标记完成,后续 REPL 的每次 prompt 提交都不会再触发文件系统检查。
七、CLAUDE.md 的发现与加载
/init 创建的 CLAUDE.md 在下一次(或同一次)会话中被自动加载。这一机制由 src/utils/claudemd.ts 和 src/context.ts 协作完成。
7.1 入口:getUserContext
src/context.ts:155-188 是上下文注入的入口:
// src/context.ts:155-188
export const getUserContext = memoize(
async (): Promise<{ [k: string]: string }> => {
const shouldDisableClaudeMd =
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
setCachedClaudeMdContent(claudeMd || null)
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}
},
)
调用链:getUserContext → getMemoryFiles() → getClaudeMds() → 注入系统提示。
7.2 多层级文件发现:getMemoryFiles
这是最复杂的部分。src/utils/claudemd.ts:790-934 的 getMemoryFiles 实现了一套多层级、多类型、向上遍历的文件发现机制:
// src/utils/claudemd.ts:790-934
export const getMemoryFiles = memoize(
async (forceIncludeExternal: boolean = false): Promise<MemoryFileInfo[]> => {
const result: MemoryFileInfo[] = []
const processedPaths = new Set<string>()
// 第一层:Managed(全局策略,始终加载)
const managedClaudeMd = getMemoryPath('Managed')
result.push(
...(await processMemoryFile(managedClaudeMd, 'Managed', processedPaths, includeExternal)),
)
// 第二层:User(用户全局配置)
if (isSettingSourceEnabled('userSettings')) {
const userClaudeMd = getMemoryPath('User')
result.push(
...(await processMemoryFile(userClaudeMd, 'User', processedPaths, true)),
)
}
// 第三层:Project + Local(项目级 + 个人级)
// 从当前目录向上遍历到根目录
let currentDir = originalCwd
while (currentDir !== parse(currentDir).root) {
dirs.push(currentDir)
currentDir = dirname(currentDir)
}
// 从根目录向下处理到当前目录
for (const dir of dirs.reverse()) {
// CLAUDE.md(Project 类型)
const projectPath = join(dir, 'CLAUDE.md')
result.push(
...(await processMemoryFile(projectPath, 'Project', processedPaths, includeExternal)),
)
// .claude/CLAUDE.md(Project 类型)
const dotClaudePath = join(dir, '.claude', 'CLAUDE.md')
result.push(
...(await processMemoryFile(dotClaudePath, 'Project', processedPaths, includeExternal)),
)
// .claude/rules/*.md(Project 类型)
const rulesDir = join(dir, '.claude', 'rules')
result.push(
...(await processMdRules({ rulesDir, type: 'Project', ... })),
)
// CLAUDE.local.md(Local 类型)
const localPath = join(dir, 'CLAUDE.local.md')
result.push(
...(await processMemoryFile(localPath, 'Local', processedPaths, includeExternal)),
)
}
},
)
发现顺序(优先级从高到低):
| 层级 | 路径 | 类型 | 用途 |
|---|---|---|---|
| 1 | /etc/claude-code/CLAUDE.md |
Managed | 全局策略(企业管理员设定) |
| 2 | ~/.claude/CLAUDE.md |
User | 用户全局偏好 |
| 3 | <project>/CLAUDE.md |
Project | 项目团队共享配置(/init 创建) |
| 3 | <project>/.claude/CLAUDE.md |
Project | 项目配置(替代位置) |
| 3 | <project>/.claude/rules/*.md |
Project | 按主题拆分的规则文件 |
| 4 | <project>/CLAUDE.local.md |
Local | 个人私有配置(gitignored) |
向上遍历的妙处
文件发现不是只看当前目录。它会从当前目录向上遍历到文件系统根,收集所有层级的 CLAUDE.md。这意味着 monorepo 可以在根目录放通用规则,各子项目放专用规则:
my-monorepo/
├── CLAUDE.md ← 根级通用规则
├── packages/
│ ├── frontend/
│ │ └── CLAUDE.md ← 前端专用规则
│ └── backend/
│ └── CLAUDE.md ← 后端专用规则
当 Claude 在 packages/frontend/ 下工作时,会同时加载根级和前端专用的 CLAUDE.md。
Git Worktree 去重
有一段精巧的 worktree 处理逻辑(src/utils/claudemd.ts:859-884):
// src/utils/claudemd.ts:868-884
const gitRoot = findGitRoot(originalCwd)
const canonicalRoot = findCanonicalGitRoot(originalCwd)
const isNestedWorktree =
gitRoot !== null &&
canonicalRoot !== null &&
normalizePathForComparison(gitRoot) !==
normalizePathForComparison(canonicalRoot) &&
pathInWorkingPath(gitRoot, canonicalRoot)
for (const dir of dirs.reverse()) {
const skipProject =
isNestedWorktree &&
pathInWorkingPath(dir, canonicalRoot) &&
!pathInWorkingPath(dir, gitRoot)
// ...
}
当在嵌套 worktree(如 .claude/worktrees/<name>/)中工作时,向上遍历会经过 worktree 根和主仓库根——两者都有 CLAUDE.md。这段代码检测到嵌套 worktree 后,跳过主仓库中的 Project 类型文件(因为 worktree 已有自己的副本),但不跳过 CLAUDE.local.md(因为它是 gitignored 的,只存在于主仓库)。
7.3 递归处理 @include
processMemoryFile(src/utils/claudemd.ts:618-685)不只是读取单个文件。它支持 @include 语法的递归展开:
// src/utils/claudemd.ts:618-685
export async function processMemoryFile(
filePath: string,
type: MemoryType,
processedPaths: Set<string>,
includeExternal: boolean,
depth: number = 0,
parent?: string,
): Promise<MemoryFileInfo[]> {
const normalizedPath = normalizePathForComparison(filePath)
if (processedPaths.has(normalizedPath) || depth >= MAX_INCLUDE_DEPTH) {
return [] // 去重 + 深度限制,防止循环引用
}
if (isClaudeMdExcluded(filePath, type)) {
return [] // 支持排除规则
}
processedPaths.add(normalizedPath)
const { info: memoryFile, includePaths: resolvedIncludePaths } =
await safelyReadMemoryFileAsync(filePath, type, resolvedPath)
if (!memoryFile || !memoryFile.content.trim()) {
return [] // 文件不存在或为空
}
const result: MemoryFileInfo[] = []
result.push(memoryFile)
// 递归处理 @include 引用
for (const resolvedIncludePath of resolvedIncludePaths) {
const isExternal = !pathInOriginalCwd(resolvedIncludePath)
if (isExternal && !includeExternal) {
continue // 外部文件需要显式批准
}
const includedFiles = await processMemoryFile(
resolvedIncludePath, type, processedPaths,
includeExternal, depth + 1, filePath,
)
result.push(...includedFiles)
}
return result
}
关键的安全设计:
processedPaths去重 — 防止循环 @include 导致无限递归MAX_INCLUDE_DEPTH深度限制 — 兜底保护- 外部文件审批 — 项目外的 @include 需要用户显式批准(
hasClaudeMdExternalIncludesApproved)
7.4 拼接为系统提示
所有收集到的文件最终通过 getClaudeMds(src/utils/claudemd.ts:1153-1195)拼接:
// src/utils/claudemd.ts:1153-1195
export const getClaudeMds = (
memoryFiles: MemoryFileInfo[],
filter?: (type: MemoryType) => boolean,
): string => {
const memories: string[] = []
for (const file of memoryFiles) {
if (filter && !filter(file.type)) continue
if (file.content) {
const description =
file.type === 'Project'
? ' (project instructions, checked into the codebase)'
: file.type === 'Local'
? " (user's private project instructions, not checked in)"
: " (user's private global instructions for all projects)"
memories.push(`Contents of ${file.path}${description}:\n\n${content}`)
}
}
return `${MEMORY_INSTRUCTION_PROMPT}\n\n${memories.join('\n\n')}`
}
每个文件带上路径和类型标注,Claude 可以区分哪些是团队共享的、哪些是个人私有的——这影响 Claude 在修改这些文件时的行为(比如不会把个人偏好写入团队配置)。
八、/init-verifiers:配套的验证器初始化
除了 /init,还有一个配套命令 /init-verifiers(src/commands/init-verifiers.ts),它专门用于创建自动验证技能:
// src/commands/init-verifiers.ts:2-10
const command = {
type: 'prompt',
name: 'init-verifiers',
description: 'Create verifier skill(s) for automated verification of code changes',
progressMessage: 'analyzing your project and creating verifier skills',
// ...
}
它同样是 prompt 类型,通过 5 个阶段完成:
- Auto-Detection — 检测项目类型(Web/CLI/API)和已有的验证工具
- Verification Tool Setup — 安装缺失的工具(Playwright、MCP 等)
- Interactive Q&A — 确认验证器名称和项目细节
- Generate Verifier Skill — 写入
.claude/skills/<verifier-name>/SKILL.md - Confirm Creation — 告知用户创建结果
验证器按类型有不同的 allowed-tools(src/commands/init-verifiers.ts:212-245):
# Playwright 验证器
allowed-tools:
- Bash(npm:*)
- mcp__playwright__*
- Read, Glob, Grep
# CLI 验证器
allowed-tools:
- Tmux
- Bash(asciinema:*)
- Read, Glob, Grep
# API 验证器
allowed-tools:
- Bash(curl:*)
- Bash(http:*)
- Read, Glob, Grep
工具集按最小权限原则配置——每种验证器只有它需要的工具。
九、设计洞察
9.1 “Prompt as Code” 模式
/init 最核心的设计是:它不是一个程序,而是一段精心编写的提示词。200 行的 NEW_INIT_PROMPT 不包含任何 TypeScript 逻辑——纯文本指令。所有的"智能"(代码分析、交互引导、文件生成)都由 Claude 模型在运行时完成。
这意味着:
- 零代码的功能扩展 — 要改变
/init的行为,只需修改 Prompt 文本 - 天然的多语言支持 — 不需要为 Python/Rust/Go 项目写不同的分析逻辑
- 自适应能力 — Claude 模型的能力提升直接带来
/init质量提升
9.2 分层配置体系
CLAUDE.md 的多层级设计借鉴了 Git 的配置分层(system → global → local):
Managed (/etc/claude-code/) ← 企业管理员
↓
User (~/.claude/) ← 个人全局
↓
Project (CLAUDE.md) ← 团队共享
↓
Local (CLAUDE.local.md) ← 个人私有
每一层可以覆盖上一层,同时 @include 支持跨层引用。
9.3 安全边界
- 外部文件
@include需要显式批准 - 路径去重防止循环引用
- 深度限制防止栈溢出
- worktree 检测防止重复加载
- CLAUDE.local.md 自动 gitignore 保护隐私
十、文件索引
| 文件 | 核心职责 |
|---|---|
src/commands/init.ts |
/init 命令定义 + OLD/NEW 两套 Prompt |
src/commands/init-verifiers.ts |
/init-verifiers 验证器创建 |
src/commands.ts |
命令注册中心 |
src/types/command.ts |
Command/PromptCommand 类型定义 |
src/utils/processUserInput/processSlashCommand.tsx |
Slash 命令处理管道 |
src/tools/FileWriteTool/FileWriteTool.ts |
CLAUDE.md 写入 + 事件追踪 |
src/projectOnboardingState.ts |
引导状态追踪 |
src/utils/claudemd.ts |
CLAUDE.md 多层级发现 + @include 递归解析 |
src/context.ts |
上下文注入(getUserContext) |
总结
/init 的实现展现了 Claude Code 的一个核心设计哲学:用 Prompt 编程,而非用代码编程。
整个 /init 命令的 TypeScript 代码不到 30 行(命令定义 + feature flag 判断),但通过 200 行精心设计的 Prompt,实现了 8 个阶段的交互式初始化流程。真正的"业务逻辑"不在代码里,而在自然语言里。
而 CLAUDE.md 的发现与加载机制则是传统工程的精华:多层级遍历、去重、循环检测、安全边界、worktree 兼容——这些是需要确定性保证的基础设施,不能交给概率性的模型。
Prompt 负责智能决策,代码负责确定性基础设施——这也许是 AI-native 工具设计的一个普适模式。
网友解答:--【壹】--:
感谢佬友分享
干货长文,学习一下。
--【贰】--:
大佬辛苦了,感谢分析,claude code 有空学习一下
--【叁】--:
学习了,真不错。感谢大佬辛苦奉献!!!!

