diff --git a/electron/ipc/git-worktree.test.ts b/electron/ipc/git-worktree.test.ts index 697af82d..37bf3ccb 100644 --- a/electron/ipc/git-worktree.test.ts +++ b/electron/ipc/git-worktree.test.ts @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { createWorktree, + ensureClaudeSandboxFiles, ensureSymlinkExcludes, getSymlinkCandidates, refreshWorktreeNodeModules, @@ -491,3 +492,49 @@ describe('ensureSymlinkExcludes', () => { expect(status).toContain('starZZfile'); }); }); + +describe('ensureClaudeSandboxFiles', () => { + function makeWorktreeDir(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'parallel-code-claude-worktree-')); + tempDirs.push(dir); + return dir; + } + + it('does not seed .claude/worktrees/ into a new worktree', () => { + const root = initRepository(); + const sourceWorktrees = path.join(root, '.claude', 'worktrees', 'some-agent-id'); + fs.mkdirSync(sourceWorktrees, { recursive: true }); + fs.writeFileSync(path.join(sourceWorktrees, 'marker.txt'), 'a nested worktree checkout\n'); + + const worktreePath = makeWorktreeDir(); + ensureClaudeSandboxFiles(worktreePath, root); + + expect(fs.existsSync(path.join(worktreePath, '.claude', 'worktrees'))).toBe(false); + }); + + it('still excludes plans/ and steps.json (pre-existing behavior)', () => { + const root = initRepository(); + fs.mkdirSync(path.join(root, '.claude', 'plans'), { recursive: true }); + fs.writeFileSync(path.join(root, '.claude', 'steps.json'), '{}\n'); + + const worktreePath = makeWorktreeDir(); + ensureClaudeSandboxFiles(worktreePath, root); + + expect(fs.existsSync(path.join(worktreePath, '.claude', 'plans'))).toBe(false); + expect(fs.existsSync(path.join(worktreePath, '.claude', 'steps.json'))).toBe(false); + }); + + it('still seeds ordinary .claude/ entries (e.g. skills/) from the source', () => { + const root = initRepository(); + const sourceSkills = path.join(root, '.claude', 'skills'); + fs.mkdirSync(sourceSkills, { recursive: true }); + fs.writeFileSync(path.join(sourceSkills, 'example.md'), '# example skill\n'); + + const worktreePath = makeWorktreeDir(); + ensureClaudeSandboxFiles(worktreePath, root); + + expect( + fs.readFileSync(path.join(worktreePath, '.claude', 'skills', 'example.md'), 'utf8'), + ).toBe('# example skill\n'); + }); +}); diff --git a/electron/ipc/git.ts b/electron/ipc/git.ts index f8508ec3..63a35458 100644 --- a/electron/ipc/git.ts +++ b/electron/ipc/git.ts @@ -149,7 +149,17 @@ const DEFAULT_SYMLINK_CANDIDATES = new Set(SYMLINK_CANDIDATES); * Entries inside `.claude/` that must NOT be seeded from the main repo's * `.claude/` into new worktrees (per-worktree-local state). */ -const CLAUDE_DIR_EXCLUDE = new Set(['plans', 'steps.json']); +const CLAUDE_DIR_EXCLUDE = new Set([ + 'plans', + 'steps.json', + // Claude Code's own runtime worktree registry. Never seed this into a new + // worktree: the main repo's .claude/worktrees/ can itself contain other + // active worktrees' full checkouts (each an independent copy of the + // repository), so copying it duplicates every one of them into every new + // task. Confirmed via `git worktree list` that entries seeded this way are + // never real registered worktrees -- always orphaned duplicate content. + 'worktrees', +]); /** * Files Claude Code's sandbox (bwrap) read-only-binds on startup. They must