这是「Claude Code 代码全景解析」专题的第 07 篇。在前几篇文章中,我们梳理了仓库结构、技术栈与构建体系。从本篇开始,我们将深入 Claude Code 的运行时启动流程,首要环节就是
setup.ts中的项目初始化与首次使用引导(Onboarding)。
一、setup.ts 概览
1.1 文件职责
src/setup.ts 是 Claude Code 启动链条中的核心初始化模块,承担以下职责:
- 环境校验:检查 Node.js 版本是否满足最低要求(≥ 18)
- 会话管理:设置自定义会话 ID、初始化 UDS(Unix Domain Socket)消息服务
- 终端恢复:检测并恢复被中断的 iTerm2 / Terminal.app 配置备份
- 工作目录设置:通过
setCwd()确立当前工作目录,后续所有文件操作均基于此 - Hooks 捕获:捕获
settings.json等 hook 配置的快照,防止运行时的隐藏修改 - Worktree 处理:支持
--worktree模式,创建独立的 git worktree 会话 - 后台任务启动:初始化 session memory、context collapse、插件预加载等
- 数据预取:提前加载 release notes、API key、命令列表等,为首次渲染做准备
1.2 与 main.tsx 的调用关系
setup() 并非在 main.tsx 的顶层同步调用,而是通过动态导入在 CLI 参数解析完成后执行(src/main.tsx,约第 1908 行):
// src/main.tsx,第 1903–1935 行(近似)
const { setup } = await import('./setup.js')
const setupPromise = setup(
preSetupCwd,
permissionMode,
allowDangerouslySkipPermissions,
worktreeEnabled,
worktreeName,
tmuxEnabled,
sessionId ? validateUuid(sessionId) : undefined,
worktreePRNumber,
messagingSocketPath,
)这里的设计意图很明确:main.tsx 负责 CLI 参数解析和高级编排,setup.ts 负责底层环境初始化。两者通过 import() 解耦,使得 setup.ts 中的重型依赖(如 chalk、bun:bundle 的 feature gate)不会阻塞 main.tsx 的早期代码加载。
值得注意的是,main.tsx 在调用 setup() 的同时,还会并行加载 getCommands() 和 getTools()(第 1913–1927 行),以重叠 I/O 耗时:setup() 的约 28ms 初始化时间与命令加载时间并行执行,显著缩短了首次提示符(first prompt)的出现时间。
二、项目检测逻辑
Claude Code 不是一个「无状态」的 CLI 工具,它的许多功能——如自动记忆、团队记忆同步、MCP 服务器配置、Trust Dialog——都依赖于对当前项目的识别。因此,启动时的项目检测是初始化的关键环节。
2.1 Git 仓库检测
Claude Code 对 Git 的检测非常彻底,核心逻辑位于 src/utils/git.ts。
逐级上溯查找 .git
findGitRoot() 函数通过向上遍历目录树来定位 Git 根目录(src/utils/git.ts,第 31–70 行):
// src/utils/git.ts,第 31–70 行
const findGitRootImpl = memoizeWithLRU(
(startPath: string): string | typeof GIT_ROOT_NOT_FOUND => {
let current = resolve(startPath)
const root = current.substring(0, current.indexOf(sep) + 1) || sep
let statCount = 0
while (current !== root) {
try {
const gitPath = join(current, '.git')
statCount++
const stat = statSync(gitPath)
// .git can be a directory (regular repo) or file (worktree/submodule)
if (stat.isDirectory() || stat.isFile()) {
return current.normalize('NFC')
}
} catch {
// .git doesn't exist at this level, continue up
}
const parent = dirname(current)
if (parent === current) {
break
}
current = parent
}
// ... 根目录检查省略
return GIT_ROOT_NOT_FOUND
},
path => path,
50,
)这里有几个值得注意的设计细节:
- 处理 worktree 和 submodule:
.git既可以是目录(普通仓库),也可以是文件(worktree/submodule),代码中通过stat.isDirectory() || stat.isFile()同时兼容两种情况。 - LRU 缓存:使用
memoizeWithLRU缓存最近 50 个路径的查找结果,避免在频繁切换目录时重复遍历。 - NFC 规范化:返回路径时调用
.normalize('NFC'),确保 Unicode 组合字符的一致性,防止后续字符串比较出现问题。
解析 canonical Git 根目录
对于 worktree 场景,findGitRoot() 返回的是 worktree 目录,而项目级配置(如 Trust Dialog、自动记忆)需要映射到主仓库。findCanonicalGitRoot() 通过读取 .git 文件中的 gitdir: 指针,再跟随 commondir 链找到主仓库(src/utils/git.ts,第 89–155 行):
// src/utils/git.ts,第 89–155 行(核心逻辑摘要)
const resolveCanonicalRoot = memoizeWithLRU((gitRoot: string): string => {
try {
const gitContent = readFileSync(join(gitRoot, '.git'), 'utf-8').trim()
if (!gitContent.startsWith('gitdir:')) {
return gitRoot
}
const worktreeGitDir = resolve(gitRoot, gitContent.slice('gitdir:'.length).trim())
const commonDir = resolve(
worktreeGitDir,
readFileSync(join(worktreeGitDir, 'commondir'), 'utf-8').trim(),
)
// 安全检查:验证 worktreeGitDir 是否位于 <commonDir>/worktrees/ 下
if (resolve(dirname(worktreeGitDir)) !== join(commonDir, 'worktrees')) {
return gitRoot
}
// 验证 backlink 是否指向当前 gitRoot
const backlink = realpathSync(
readFileSync(join(worktreeGitDir, 'gitdir'), 'utf-8').trim(),
)
if (backlink !== join(realpathSync(gitRoot), '.git')) {
return gitRoot
}
// ... 返回主仓库工作目录
} catch {
return gitRoot
}
})这里的安全检查尤其值得称道:Claude Code 明确考虑了恶意仓库通过伪造 .git 文件和 commondir 来指向受害者已信任的目录,从而绕过 Trust Dialog 的攻击向量。两道验证(路径结构验证 + backlink 验证)确保了 worktree 解析的安全性。
2.2 包管理器与运行时检测
在环境检测方面,src/utils/env.ts 提供了一组**惰性缓存(memoized)**的检测函数(第 39–58 行):
// src/utils/env.ts,第 39–58 行
const detectPackageManagers = memoize(async (): Promise<string[]> => {
const packageManagers = []
if (await isCommandAvailable('npm')) packageManagers.push('npm')
if (await isCommandAvailable('yarn')) packageManagers.push('yarn')
if (await isCommandAvailable('pnpm')) packageManagers.push('pnpm')
return packageManagers
})
const detectRuntimes = memoize(async (): Promise<string[]> => {
const runtimes = []
if (await isCommandAvailable('bun')) runtimes.push('bun')
if (await isCommandAvailable('deno')) runtimes.push('deno')
if (await isCommandAvailable('node')) runtimes.push('node')
return runtimes
})检测方式非常朴素但有效:通过 which 命令检查可执行文件是否存在。memoize 确保这些检测在单次会话中只执行一次。虽然目前 setup.ts 本身并未直接消费这些检测结果,但它们为后续的工具调用(如 BashTool 选择正确的包管理器)提供了环境上下文。
2.3 仓库识别(GitHub owner/name)
src/utils/detectRepository.ts 负责从 Git remote URL 中解析出仓库的 host、owner 和 name(第 24–88 行):
// src/utils/detectRepository.ts,第 24–88 行
export async function detectCurrentRepositoryWithHost(): Promise<ParsedRepository | null> {
const cwd = getCwd()
if (repositoryWithHostCache.has(cwd)) {
return repositoryWithHostCache.get(cwd) ?? null
}
try {
const remoteUrl = await getRemoteUrl()
if (!remoteUrl) {
repositoryWithHostCache.set(cwd, null)
return null
}
const parsed = parseGitRemote(remoteUrl)
repositoryWithHostCache.set(cwd, parsed)
return parsed
} catch (error) {
repositoryWithHostCache.set(cwd, null)
return null
}
}parseGitRemote() 支持五种常见的 Git remote URL 格式:
git@host:owner/repo.git(SSH 格式)https://host/owner/repo.gitssh://git@host/owner/repo.gitgit://host/owner/repo.githttps://host/owner/repo(无.git后缀)
此外还包含一个 looksLikeRealHostname() 辅助函数,用于过滤掉 SSH config alias(如 github.com-work),确保解析结果的可信度。
三、Onboarding 流程
Claude Code 的 Project Onboarding 是引导用户在项目中完成首次配置的状态机系统。它的核心实现位于 src/projectOnboardingState.ts,逻辑非常精简,却设计得相当精巧。
3.1 Onboarding 状态设计
projectOnboardingState.ts 定义了一个 Step 类型和一个 getSteps() 函数(第 9–31 行):
// src/projectOnboardingState.ts,第 9–31 行
export type Step = {
key: string
text: string
isComplete: boolean
isCompletable: boolean
isEnabled: boolean
}
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,
},
]
}这里的设计体现了条件分支 onboarding:
- 如果当前目录为空(
isWorkspaceDirEmpty),启用workspace步骤,引导用户创建新项目或克隆仓库 - 如果当前目录非空,启用
claudemd步骤,引导用户运行/init创建CLAUDE.md
两个步骤互斥,根据目录状态自动切换。
3.2 Onboarding 完成判定
完成判定逻辑非常简单(第 33–37 行):
// src/projectOnboardingState.ts,第 33–37 行
export function isProjectOnboardingComplete(): boolean {
return getSteps()
.filter(({ isCompletable, isEnabled }) => isCompletable && isEnabled)
.every(({ isComplete }) => isComplete)
}它只检查当前启用的、可完成的步骤是否全部完成。这意味着如果目录为空,用户只需要完成 workspace 步骤;如果目录非空,只需要创建 CLAUDE.md。
3.3 可见性控制
shouldShowProjectOnboarding() 函数控制 onboarding 提示的显示逻辑(第 45–57 行):
// src/projectOnboardingState.ts,第 45–57 行
export const shouldShowProjectOnboarding = memoize((): boolean => {
const projectConfig = getCurrentProjectConfig()
if (
projectConfig.hasCompletedProjectOnboarding ||
projectConfig.projectOnboardingSeenCount >= 4 ||
process.env.IS_DEMO
) {
return false
}
return !isProjectOnboardingComplete()
})可见性控制的策略是:
- 已标记完成(
hasCompletedProjectOnboarding)→ 不再显示 - 已展示 4 次(
projectOnboardingSeenCount >= 4)→ 不再打扰用户 - DEMO 模式(
IS_DEMO环境变量)→ 跳过 onboarding - 步骤已全部完成 → 自动隐藏
这个「最多展示 4 次」的设计非常人性化——即使玩家暂时没有完成 onboarding,也不会被反复弹窗骚扰。
3.4 计数与持久化
每次展示 onboarding 时,会调用 incrementProjectOnboardingSeenCount() 增加计数器(第 59–63 行):
// src/projectOnboardingState.ts,第 59–63 行
export function incrementProjectOnboardingSeenCount(): void {
saveCurrentProjectConfig(current => ({
...current,
projectOnboardingSeenCount: current.projectOnboardingSeenCount + 1,
}))
}当所有步骤完成时,maybeMarkProjectOnboardingComplete() 会将 hasCompletedProjectOnboarding 标记为 true(第 39–47 行):
// src/projectOnboardingState.ts,第 39–47 行
export function maybeMarkProjectOnboardingComplete(): void {
if (getCurrentProjectConfig().hasCompletedProjectOnboarding) {
return
}
if (isProjectOnboardingComplete()) {
saveCurrentProjectConfig(current => ({
...current,
hasCompletedProjectOnboarding: true,
}))
}
}注意这里的短路优化:函数开头先检查缓存的 hasCompletedProjectOnboarding,如果已经为 true 则直接返回,避免不必要的文件系统访问。注释中特别提到,这个优化是必要的,因为 REPL.tsx 在每次提交 prompt 时都会调用此函数。
3.5 Onboarding 流程图
flowchart TD
A[启动 Claude Code] --> B{目录是否为空?}
B -->|是| C[启用 workspace 步骤
引导创建/克隆项目]
B -->|否| D[启用 claudemd 步骤
引导运行 /init]
C --> E{用户是否完成步骤?}
D --> E
E -->|否| F{展示次数 < 4?}
F -->|是| G[继续展示 Onboarding 提示]
F -->|否| H[永久隐藏提示]
E -->|是| I[标记 hasCompletedProjectOnboarding = true]
I --> J[Onboarding 完成]
G --> K[projectOnboardingSeenCount + 1]
K --> L[等待下次交互]四、配置初始化
Claude Code 的配置体系分为全局配置(GlobalConfig)和项目配置(ProjectConfig)两层,两者共同存储在用户的 home 目录下的 JSON 文件中。
4.1 配置存储位置
全局配置文件的路径由 getGlobalClaudeFile() 决定(src/utils/env.ts,第 14–22 行):
// src/utils/env.ts,第 14–22 行
export const getGlobalClaudeFile = memoize((): string => {
// Legacy fallback for backwards compatibility
if (
getFsImplementation().existsSync(
join(getClaudeConfigHomeDir(), '.config.json'),
)
) {
return join(getClaudeConfigHomeDir(), '.config.json')
}
const filename = `.claude${fileSuffixForOauthConfig()}.json`
return join(process.env.CLAUDE_CONFIG_DIR || homedir(), filename)
})默认情况下,配置文件位于 ~/.claude.json(或带 OAuth 后缀的变体)。这个文件中同时包含:
GlobalConfig:主题、编辑器模式、自动更新、OAuth 账户等用户级设置Record<string, ProjectConfig>:以项目路径为 key 的项目级配置字典
4.2 项目配置键的生成策略
项目配置不是以当前工作目录(cwd)直接作为 key,而是通过 getProjectPathForConfig() 智能映射(src/utils/config.ts,第 1588–1600 行):
// src/utils/config.ts,第 1588–1600 行
export const getProjectPathForConfig = memoize((): string => {
const originalCwd = getOriginalCwd()
const gitRoot = findCanonicalGitRoot(originalCwd)
if (gitRoot) {
return normalizePathForConfigKey(gitRoot)
}
// Not in a git repo
return normalizePathForConfigKey(resolve(originalCwd))
})这一设计确保了:
- Git 仓库内:无论你在项目的哪个子目录启动 Claude Code,配置都映射到同一个 git root
- Worktree 场景:通过
findCanonicalGitRoot(),所有 worktree 共享主仓库的配置 - 非 Git 目录:回退到原始启动目录(resolved)
路径使用 normalizePathForConfigKey() 统一转换为正斜杠格式,保证跨平台一致性(Windows 上的 C:\Users\... 和 C:/Users/... 映射到同一个 key)。
4.3 默认项目配置
ProjectConfig 的默认值定义在 DEFAULT_PROJECT_CONFIG 中(src/utils/config.ts,第 110–123 行):
// src/utils/config.ts,第 110–123 行
const DEFAULT_PROJECT_CONFIG: ProjectConfig = {
allowedTools: [],
mcpContextUris: [],
mcpServers: {},
enabledMcpjsonServers: [],
disabledMcpjsonServers: [],
hasTrustDialogAccepted: false,
projectOnboardingSeenCount: 0,
hasClaudeMdExternalIncludesApproved: false,
hasClaudeMdExternalIncludesWarningShown: false,
}关键字段含义:
| 字段 | 说明 |
|---|---|
allowedTools | 用户已授权的工具白名单 |
hasTrustDialogAccepted | 是否已接受 Trust Dialog(项目信任确认) |
hasCompletedProjectOnboarding | 项目级 onboarding 是否已完成 |
projectOnboardingSeenCount | onboarding 提示已展示次数 |
mcpServers | 项目级 MCP 服务器配置 |
activeWorktreeSession | 当前活动的 worktree 会话信息 |
4.4 CLAUDE.md 的创建建议
在 onboarding 的 claudemd 步骤中,Claude Code 引导用户运行 /init 命令来创建 CLAUDE.md。这个文件是项目级上下文注入的核心载体——开发者可以在其中写入项目的编码规范、架构约定、常用命令等,Claude Code 会在每次会话开始时自动读取并将其注入系统提示词。
setup.ts 本身并不直接创建 CLAUDE.md,但它在初始化阶段会调用 clearMemoryFileCaches()(在 worktree 切换后),确保内存中的 CLAUDE.md 缓存被清除,以便重新读取最新内容。
4.5 配置的读写与并发安全
saveCurrentProjectConfig() 实现了带锁的配置写入(src/utils/config.ts,第 1625–1675 行):
// src/utils/config.ts,第 1625–1675 行(核心逻辑摘要)
export function saveCurrentProjectConfig(
updater: (currentConfig: ProjectConfig) => ProjectConfig,
): void {
const absolutePath = getProjectPathForConfig()
let written: GlobalConfig | null = null
try {
const didWrite = saveConfigWithLock(
getGlobalClaudeFile(),
createDefaultGlobalConfig,
current => {
const currentProjectConfig =
current.projects?.[absolutePath] ?? DEFAULT_PROJECT_CONFIG
const newProjectConfig = updater(currentProjectConfig)
// Skip if no changes (same reference returned)
if (newProjectConfig === currentProjectConfig) {
return current
}
written = {
...current,
projects: {
...current.projects,
[absolutePath]: newProjectConfig,
},
}
return written
},
)
if (didWrite && written) {
writeThroughGlobalConfigCache(written)
}
} catch (error) {
// 降级处理:直接读写,但会检查是否会导致 auth 状态丢失
// ...
}
}这里有几个工程亮点:
- 函数式更新:接收
updater回调而非直接传入新值,保证更新基于最新状态 - 引用相等优化:如果
updater返回的引用与输入相同,跳过写入 - 文件锁:
saveConfigWithLock()使用 lockfile 防止并发写入冲突 - Auth 安全降级:如果带锁写入失败,降级为直接写入,但会检查是否会导致认证状态丢失(参考 GitHub issue #3117)
五、setup.ts 中的其他初始化细节
5.1 Node.js 版本检查
setup() 的第一件事就是检查 Node.js 版本(第 31–41 行):
// src/setup.ts,第 31–41 行
const nodeVersion = process.version.match(/^v(\d+)\./)?.[1]
if (!nodeVersion || parseInt(nodeVersion) < 18) {
console.error(
chalk.bold.red(
'Error: Claude Code requires Node.js version 18 or higher.',
),
)
process.exit(1)
}这是整个函数中唯一会导致进程直接退出的检查点,优先级最高。
5.2 UDS 消息服务
当 UDS_INBOX feature flag 开启时,setup() 会启动 Unix Domain Socket 消息服务器(第 49–61 行):
// src/setup.ts,第 49–61 行
if (feature('UDS_INBOX')) {
const m = await import('./utils/udsMessaging.js')
await m.startUdsMessaging(
messagingSocketPath ?? m.getDefaultUdsSocketPath(),
{ isExplicit: messagingSocketPath !== undefined },
)
}这个服务用于多会话间的进程通信,例如 claude remote-control 功能。它被设计为在 setup() 中 await,确保在 SessionStart hook 可能产生子进程之前,环境变量 $CLAUDE_CODE_MESSAGING_SOCKET 已经就绪。
5.3 终端备份恢复
Claude Code 支持为 iTerm2 和 Terminal.app 自动安装键位绑定(如 Shift+Enter 发送消息)。如果之前的安装过程被中断,setup() 会检测备份文件并恢复原始配置(第 73–111 行)。这一机制保护了用户的终端环境不会因为 Claude Code 的自动配置而被永久改变。
六、总结
Claude Code 的初始化流程虽然代码量大,但设计层次分明:
| 层级 | 职责 | 代表文件 |
|---|---|---|
| CLI 编排层 | 参数解析、子命令分发、初始化调度 | main.tsx |
| 环境初始化层 | 目录检测、配置加载、Hook 捕获 | setup.ts |
| 项目状态层 | Onboarding 状态机、配置读写 | projectOnboardingState.ts |
| 工具层 | Git 检测、仓库解析、文件操作 | git.ts、detectRepository.ts、config.ts |
从工程角度看,setup.ts 展示了工业级 CLI 工具的几个最佳实践:
- 延迟加载:通过动态
import()避免重型模块阻塞启动路径 - 并行 I/O:
main.tsx中setup()与getCommands()并行执行,最大化重叠耗时 - 防御性编程:worktree 解析时的双重安全检查、配置写入时的 auth 状态保护
- 渐进式引导:Onboarding 不强制一次性完成,允许用户最多看到 4 次提示
- 跨平台兼容:路径的 NFC 规范化、正斜杠统一、Windows / macOS / Linux 的差异化处理
理解这套初始化机制,是后续深入分析 Claude Code 的 Tool 系统、Agent 编排和 MCP 生态的前提——因为它们都依赖于 setup.ts 所确立的项目上下文和配置基础。
参考源码:
src/setup.ts:yasasbanukaofficial/claude-codesrc/projectOnboardingState.ts:yasasbanukaofficial/claude-codesrc/utils/detectRepository.ts:yasasbanukaofficial/claude-codesrc/utils/git.ts:yasasbanukaofficial/claude-codesrc/utils/config.ts:yasasbanukaofficial/claude-codesrc/utils/env.ts:yasasbanukaofficial/claude-code