Summary
copy_directories in lib/copy.sh copies the full parent directory first (via _fast_copy_dir), then calls _apply_directory_excludes to rm -rf the excluded subdirectories afterwards. For large excluded subtrees this means paying the full clone + delete cost every time a worktree is created, even though the data is immediately thrown away.
Reproduction
Add a large directory to includeDirs and exclude a large subdirectory of it via excludeDirs:
[copy]
includeDirs = .claude
excludeDirs = .claude/worktrees
Where .claude/worktrees contains ~150,000 files / 3.1 GB (Claude Code session state).
Running git gtr new <branch> takes ~70 extra seconds per worktree:
- ~48s for
cp -cRP .claude <worktree>/ (APFS clonefile still allocates per-inode metadata for 150k files)
- ~22s for
rm -rf <worktree>/.claude/worktrees
Root cause
In copy_directories (line 369), is_excluded is checked against the top-level dir_path (e.g. .claude). The exclude patterns (e.g. .claude/worktrees) don't match .claude, so the full directory is cloned. _apply_directory_excludes is only called at line 381 after _fast_copy_dir has already completed.
# copy.sh, inside copy_directories:
is_excluded "$dir_path" "$excludes" && continue # .claude doesn't match .claude/worktrees → proceeds
_fast_copy_dir "$dir_path" "$dest_parent/" # clones ALL 3.1 GB including worktrees
_apply_directory_excludes "$dest_parent" "$dir_path" "$excludes" # rm -rf worktrees after the fact
Expected behaviour
When excludeDirs specifies a direct child of an includeDirs directory, the excluded subtree should be skipped during the copy rather than deleted afterwards. On macOS this could be achieved with rsync --exclude or by walking one level and selectively cloning; on Linux cp --reflink=auto combined with pre-filtering would work similarly.
Workaround
Replace broad includeDirs = .parent entries with specific includeDirs and include entries for only the subdirectories/files you actually need, avoiding the large excluded subtree entirely.
Summary
copy_directoriesinlib/copy.shcopies the full parent directory first (via_fast_copy_dir), then calls_apply_directory_excludestorm -rfthe excluded subdirectories afterwards. For large excluded subtrees this means paying the full clone + delete cost every time a worktree is created, even though the data is immediately thrown away.Reproduction
Add a large directory to
includeDirsand exclude a large subdirectory of it viaexcludeDirs:Where
.claude/worktreescontains ~150,000 files / 3.1 GB (Claude Code session state).Running
git gtr new <branch>takes ~70 extra seconds per worktree:cp -cRP .claude <worktree>/(APFSclonefilestill allocates per-inode metadata for 150k files)rm -rf <worktree>/.claude/worktreesRoot cause
In
copy_directories(line 369),is_excludedis checked against the top-leveldir_path(e.g..claude). The exclude patterns (e.g..claude/worktrees) don't match.claude, so the full directory is cloned._apply_directory_excludesis only called at line 381 after_fast_copy_dirhas already completed.Expected behaviour
When
excludeDirsspecifies a direct child of anincludeDirsdirectory, the excluded subtree should be skipped during the copy rather than deleted afterwards. On macOS this could be achieved withrsync --excludeor by walking one level and selectively cloning; on Linuxcp --reflink=autocombined with pre-filtering would work similarly.Workaround
Replace broad
includeDirs = .parententries with specificincludeDirsandincludeentries for only the subdirectories/files you actually need, avoiding the large excluded subtree entirely.