Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions electron/ipc/git-worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
createWorktree,
ensureClaudeSandboxFiles,
ensureSymlinkExcludes,
getSymlinkCandidates,
refreshWorktreeNodeModules,
Expand Down Expand Up @@ -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');
});
});
12 changes: 11 additions & 1 deletion electron/ipc/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down