From b026828d8d9cef1d6876268cf6a19ff2fbf5063c Mon Sep 17 00:00:00 2001 From: sneha jose Date: Wed, 26 Aug 2026 15:33:54 +0530 Subject: [PATCH] fix(worktree): exclude .claude/worktrees/ from being seeded into new tasks CLAUDE_DIR_EXCLUDE already exists specifically to keep per-worktree-local state out of new tasks (plans/, steps.json), but it was missing 'worktrees' -- Claude Code's own runtime worktree registry. Since .claude/worktrees/ in the main repo can itself contain other currently-active worktrees (each a full independent checkout of the project), seeding it into every new task duplicates all of them, every time. In a repo with a handful of active Claude Code worktrees this added ~1.4-1.9GB of pure duplication per new task -- verified live across multiple task creations in a real project -- making task creation and later worktree removal both noticeably slow. Confirmed via 'git worktree list' that anything seeded this way is never a real registered worktree of the target repo; it's always orphaned duplicate content from the copy. Adds test coverage for ensureClaudeSandboxFiles (previously untested): worktrees/ is excluded, plans/ and steps.json remain excluded, and ordinary entries (e.g. skills/) are still seeded correctly. --- electron/ipc/git-worktree.test.ts | 47 +++++++++++++++++++++++++++++++ electron/ipc/git.ts | 12 +++++++- 2 files changed, 58 insertions(+), 1 deletion(-) 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