Claude Code init 命令深度源码解析:从输入到 CLAUDE.md 生成的完整链路

2026-04-13 12:301阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

前言

当我们首次在一个项目中使用 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 API
  • contentLength: 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 testcargo 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 完成标记

/initgetPromptForCommand 开头就调用了 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, })) } }

注意两层检查

  1. 先检查配置缓存(避免每次都读文件系统)
  2. 再检查实际完成状态

一旦标记完成,后续 REPL 的每次 prompt 提交都不会再触发文件系统检查。


七、CLAUDE.md 的发现与加载

/init 创建的 CLAUDE.md 在下一次(或同一次)会话中被自动加载。这一机制由 src/utils/claudemd.tssrc/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()}.`, } }, )

调用链:getUserContextgetMemoryFiles()getClaudeMds() → 注入系统提示。

7.2 多层级文件发现:getMemoryFiles

这是最复杂的部分。src/utils/claudemd.ts:790-934getMemoryFiles 实现了一套多层级、多类型、向上遍历的文件发现机制:

// 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

processMemoryFilesrc/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 拼接为系统提示

所有收集到的文件最终通过 getClaudeMdssrc/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-verifierssrc/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 个阶段完成:

  1. Auto-Detection — 检测项目类型(Web/CLI/API)和已有的验证工具
  2. Verification Tool Setup — 安装缺失的工具(Playwright、MCP 等)
  3. Interactive Q&A — 确认验证器名称和项目细节
  4. Generate Verifier Skill — 写入 .claude/skills/<verifier-name>/SKILL.md
  5. 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 API
  • contentLength: 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 testcargo 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 完成标记

/initgetPromptForCommand 开头就调用了 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, })) } }

注意两层检查

  1. 先检查配置缓存(避免每次都读文件系统)
  2. 再检查实际完成状态

一旦标记完成,后续 REPL 的每次 prompt 提交都不会再触发文件系统检查。


七、CLAUDE.md 的发现与加载

/init 创建的 CLAUDE.md 在下一次(或同一次)会话中被自动加载。这一机制由 src/utils/claudemd.tssrc/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()}.`, } }, )

调用链:getUserContextgetMemoryFiles()getClaudeMds() → 注入系统提示。

7.2 多层级文件发现:getMemoryFiles

这是最复杂的部分。src/utils/claudemd.ts:790-934getMemoryFiles 实现了一套多层级、多类型、向上遍历的文件发现机制:

// 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

processMemoryFilesrc/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 拼接为系统提示

所有收集到的文件最终通过 getClaudeMdssrc/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-verifierssrc/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 个阶段完成:

  1. Auto-Detection — 检测项目类型(Web/CLI/API)和已有的验证工具
  2. Verification Tool Setup — 安装缺失的工具(Playwright、MCP 等)
  3. Interactive Q&A — 确认验证器名称和项目细节
  4. Generate Verifier Skill — 写入 .claude/skills/<verifier-name>/SKILL.md
  5. 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 有空学习一下


--【叁】--:

学习了,真不错。感谢大佬辛苦奉献!!!!

标签:软件开发