Skip to content
源码分析手册

git-worktrees:打造开发会话的无损隔离沙箱

对应官方文档:claude-code-docs/docs/common-workflows.md 里的 Run parallel Claude Code sessions with Git worktreesCopy gitignored files to worktrees

先搞清楚这是什么

--worktree(以及配套的 .worktreeinclude)最容易被误解为一种顺带手运行的 Git 小工具:帮你建个目录、切个分支,然后就和普通会话没区别了。

但在源码里,它的定位是会话级的“隔离容器”。 Claude Code 不是简单地创建一个 git worktree,而是把后续这一整场 session 的核心状态(CWD、Project Root、Settings 读取、退出清理决策以及 --resume 恢复逻辑)全部切到了那份副本之上。它解决的是在不污染主仓库、不丢失本地配置的前提下,进行并行、可丢弃、可恢复的开发实验。

运行时的真相

实现这套“容器化会话”的核心邏辑在于 src/utils/worktree.ts

  1. 会话外壳初始化:启动时检测到 --worktreesrc/setup.ts 会先执行 createWorktreeForSession(...)。这里会生成一个 slug 标识符,并进行严格校验,防止 worktree 路径利用 .. 逃逸出 .claude/worktrees/
  2. 创建与恢复路径
    • Git 默认后端:源码并不总是执行 git worktree add。它会先通过 readWorktreeHeadSha 探测目标目录是否已有合法的 Git 指针。如果存在,则走“快回(Fast Resume)”路径,实现无损的会话恢复。
    • 默认分支选择:基线分支的确定在 src/utils/git/gitFilesystem.ts。它优先读取本地 refs/remotes/origin/HEAD 的指向,找不到才回退到 main/master 探测。这解释了为什么它不需要联网就能分叉。
  3. 播种(Post-creation Setup):新副本创建后,performPostCreationSetup(...) 会同步几项关键物料,让它“立即具备生产力”:
    • Settings 同步:复制 settings.local.json
    • Hook 共享:通过 core.hooksPath 让 worktree 共享主仓库的 Git 挂载逻辑。
    • .worktreeinclude 白名单:这是一个双重过滤逻辑(模式匹配 + 事实被 Git 忽略)。源码会扫描 .worktreeinclude 语法,只把那些本应被 gitignore 但被你显式点名的本地物料(如 .env)抄过去。它通过 git ls-files --others --ignored 进行确认,避免了盲目复制。
  4. 清理与退出WorktreeExitDialog.tsx 里的逻辑非常人性化。如果会话结束时 worktree 是干净的(无未提交改动/无新增 commit),它会静默自动清理;如果有改动,则弹窗让用户选择 keep(保留副本及分支以供下次 resume)或 cleanup(物理删除)。

使用时的关键约束

  1. 双重过滤约束.worktreeinclude 只能抄那些被 gitignore 的文件。如果一个文件没进 .gitignore 也没被 track,写在 include 里也是白费,源码不会去碰。
  2. 后端可插拔性:虽然默认是 Git,但源码预留了 WorktreeCreate / WorktreeRemove hook。如果你配置了这些 hook,Claude Code 可以将“隔离副本”交给自定义的后端。
  3. 不改变持久设置:worktree 是会话级的。虽然你可以修改 worktree 内的 .claude/settings.json,但这不会反向影响你主仓库的设置。
  4. Mid-session 差异:工具 /enter_worktree 与启动参数 --worktree 底层逻辑一致,但前者不会修改 Project Root,更像是一次临时任务分支;而后者将副本视为整个 session 的根本。

推荐阅读路径

  • 如果你关心本地配置如何在不同 worktree 间共享,下一篇该看 settings / hierarchy
  • 如果你关心 worktree 失败时如何自动回滚,去看 worktree.ts 里的 tearDown 逻辑
  • 如果你想看并行 session 为什么能自动隔离,去看 bridgeMain.ts 里的 spawnMode === 'worktree' 分流

源码锚点

  • claude-code-opensource/src/utils/worktree.ts:整个 worktree 机制的核心:路径校验、创建/恢复、副本播种、清理。
📄 src/utils/worktree.ts — 整个 worktree 机制的核心:路径校验、创建/恢复、副本播种、清理。L48-77 of 1520
typescript
const VALID_WORKTREE_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/
const MAX_WORKTREE_SLUG_LENGTH = 64

/**
 * Validates a worktree slug to prevent path traversal and directory escape.
 *
 * The slug is joined into `.claude/worktrees/<slug>` via path.join, which
 * normalizes `..` segments — so `../../../target` would escape the worktrees
 * directory. Similarly, an absolute path (leading `/` or `C:\`) would discard
 * the prefix entirely.
 *
 * Forward slashes are allowed for nesting (e.g. `asm/feature-foo`); each
 * segment is validated independently against the allowlist, so `.` / `..`
 * segments and drive-spec characters are still rejected.
 *
 * Throws synchronously — callers rely on this running before any side effects
 * (git commands, hook execution, chdir).
 */
export function validateWorktreeSlug(slug: string): void {
  if (slug.length > MAX_WORKTREE_SLUG_LENGTH) {
    throw new Error(
      `Invalid worktree name: must be ${MAX_WORKTREE_SLUG_LENGTH} characters or fewer (got ${slug.length})`,
    )
  }
  // Leading or trailing `/` would make path.join produce an absolute path
  // or a dangling segment. Splitting and validating each segment rejects
  // both (empty segments fail the regex) while allowing `user/feature`.
  for (const segment of slug.split('/')) {
    if (segment === '.' || segment === '..') {
      throw new Error(
  • claude-code-opensource/src/utils/git/gitFilesystem.ts:默认分支探测与基线选择逻辑。
📄 src/utils/git/gitFilesystem.ts — 默认分支探测与基线选择逻辑。L28-33 of 700
typescript
const resolveGitDirCache = new Map<string, string | null>()

/** Clear cached git dir resolutions. Exported for testing only. */
export function clearResolveGitDirCache(): void {
  resolveGitDirCache.clear()
}
  • claude-code-opensource/src/setup.ts--worktree 启动路径与 CWD/Project Root 切换。
📄 src/setup.ts — `--worktree` 启动路径与 CWD/Project Root 切换。L15-21 of 478
typescript
  getProjectRoot,
  getSessionId,
  setOriginalCwd,
  setProjectRoot,
  switchSession,
} from './bootstrap/state.js'
import { getCommands } from './commands.js'
  • claude-code-opensource/src/components/WorktreeExitDialog.tsx:决定是“静默清理”还是“弹窗询问”的裁决逻辑。
📄 src/components/WorktreeExitDialog.tsx — 决定是“静默清理”还是“弹窗询问”的裁决逻辑。L17-22 of 231
tsx
function recordWorktreeExit(): void {
  /* eslint-disable @typescript-eslint/no-require-imports */
  ;
  (require('../utils/sessionStorage.js') as typeof import('../utils/sessionStorage.js')).saveWorktreeState(null);
  /* eslint-enable @typescript-eslint/no-require-imports */
}
  • claude-code-opensource/src/utils/sessionRestore.ts--resume 如何拉起存量 worktree 状态。
📄 src/utils/sessionRestore.ts — `--resume` 如何拉起存量 worktree 状态。L17-22 of 552
typescript
  type AgentDefinition,
  type AgentDefinitionsResult,
  getActiveAgentsFromList,
  getAgentDefinitionsWithOverrides,
} from '../tools/AgentTool/loadAgentsDir.js'
import { TODO_WRITE_TOOL_NAME } from '../tools/TodoWriteTool/constants.js'

基于 Claude Code v2.1.88 开源快照的深度分析