[BUG] .worktreeinclude files are no longer APFS-cloned on macOS since 2.1.275 (full physical copies)

Status Open
Reported on v2.1.275
Maintainer reply None cached
Activity 0 comments · opened Oct 5, 2026

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 .worktreeinclude loop called fs/promises copyFile(src, dest) with no flags. On macOS, Bun's copyFile uses clonefile() 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 (default true):
  • Flag off: it uses the old copyFile path, which clones.
  • Flag on: it stages the copy. It opens the source with O_RDONLY|O_NOFOLLOW|O_NONBLOCK, runs pipeline(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 to copy_file_range.
  • Simpler alternative: copyFile(src, tmp, COPYFILE_FICLONE), then compare the source's dev/ino against the opened descriptor's fstat before 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 worktreeDepSeed clonefile freeze). Neither is about .worktreeinclude.
  • Current workaround: a WorktreeCreate hook that does its own copy with cp -c. That hook replaces worktree creation entirely, though, so it's a heavy substitute for one copy call.

View original on GitHub ↗