[BUG] .worktreeinclude files are no longer APFS-cloned on macOS since 2.1.275 (full physical copies)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
On macOS (APFS), files copied into a new worktree via .worktreeinclude are now full physical copies instead of APFS copy-on-write clones. This started in 2.1.275 (published 2026-09-17) and is still the case in 2.1.289.
For repos that keep large gitignored caches in .worktreeinclude, this means every worktree now costs the full size of those files on disk. In our case it's about 9.4 GB per worktree, and .claude/worktrees reached ~200 GB across 22 worktrees, where clones used to cost almost nothing until written.
Cause, from reading the bundled JS:
- Up to 2.1.274, the
.worktreeincludeloop calledfs/promisescopyFile(src, dest)with no flags. On macOS, Bun'scopyFileusesclonefile()for regular files over 128 KB, so large files were cloned. This was never documented, but it worked. - From 2.1.275, the loop calls a new helper gated on the feature flag
tengu_squishy_spring(defaulttrue): - Flag off: it uses the old
copyFilepath, which clones. - Flag on: it stages the copy. It opens the source with
O_RDONLY|O_NOFOLLOW|O_NONBLOCK, runspipeline(fh.createReadStream(), createWriteStream(tmp, {flags: "wx"})), then renames the temp file onto the destination.
A streamed byte copy can never produce a clone. The hardening against symlinks and files swapped mid-copy looks intentional; losing copy-on-write looks like a side effect. The changelog for 2.1.275 doesn't mention this change.
What Should Happen?
On copy-on-write filesystems, .worktreeinclude files should be cloned, as they were through 2.1.274, while keeping the new safety checks. Some possible approaches:
- macOS: clone from the already-opened,
O_NOFOLLOW-checked file descriptor into the staging path (fclonefileat(srcFd, AT_FDCWD, tmp, 0)), then rename as now. Because the clone is taken from the verified descriptor, it keeps the swap protection. - Linux (btrfs/XFS):
ioctl(FICLONE)from the opened descriptor, falling back tocopy_file_range. - Simpler alternative:
copyFile(src, tmp, COPYFILE_FICLONE), then compare the source'sdev/inoagainst the opened descriptor'sfstatbefore the rename, so a swapped source is still refused. - In every case, fall back to the current streamed copy when cloning isn't supported (different volume, non-CoW filesystem).
Error Messages/Logs
No errors. Copying succeeds, but each file is allocated in full on disk.
Steps to Reproduce
On macOS with APFS:
mkdir /tmp/wt-clone-test && cd /tmp/wt-clone-test
git init -q
printf 'big.bin\n' > .gitignore
cp .gitignore .worktreeinclude
git add . && git commit -qm init
head -c 1073741824 /dev/urandom > big.bin # 1 GiB gitignored file
df -k . | awk 'NR==2{print "free KiB before:", $4}'
claude --worktree t -p "reply ok" < /dev/null
df -k . | awk 'NR==2{print "free KiB after: ", $4}'
- 2.1.274: free space drops by roughly 0 (the file is cloned).
- 2.1.275 – 2.1.289: free space drops by ~1 GiB (the file is fully copied).
I bisected with the @anthropic-ai/claude-code-darwin-arm64 npm binaries: 2.1.200, 2.1.250, 2.1.265, 2.1.272 and 2.1.274 clone; 2.1.275, 2.1.276, 2.1.278, 2.1.280 and 2.1.289 copy. Clone vs copy was verified per file by comparing physical block addresses (fcntl(F_LOG2PHYS_EXT)) of source and destination at sampled offsets, which returned 64/64 shared blocks versus 0/64. Every build embeds the same Bun (v1.4.3), so the runtime didn't change.
Claude Model
Not sure / Multiple models (not model-related)
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.274
Claude Code Version
2.1.289 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
zsh
Additional Information
- Related but distinct: #92078 (Desktop pooled-worktree disk growth) and #93382 (Desktop
worktreeDepSeedclonefile freeze). Neither is about.worktreeinclude. - Current workaround: a
WorktreeCreatehook that does its own copy withcp -c. That hook replaces worktree creation entirely, though, so it's a heavy substitute for one copy call.