Different Directories, Same Dependency File
A hardlink optimization gave each worktree its own dependency directory but shared the mutable files underneath. The regression test had been asserting the property we needed to eliminate.
A dependency-seeding optimization in my worktree creator passed its tests and broke the isolation the creator was supposed to provide.
The optimization copied webui/node_modules from an existing checkout when the two lockfiles matched. It used cp -al: recursively create hardlinks instead of copying file contents. Each worktree got a separate directory tree without allocating a second copy of every dependency file.
Then a follow-up check wrote to one of those files. The other checkout changed too.
If you run parallel coding agents, this is an easy boundary to miss. Separate branches and separate dependency directories do not necessarily mean separate mutable state.
The test proved the wrong property
Earlier this month, I wrote about preparing dependency environments when creating worktrees. One requirement was to stop a dependency operation in a new checkout from writing through a node_modules symlink into another checkout.
The later hardlink optimization avoided that directory symlink. But it reintroduced sharing one level down: the files themselves.
A hardlink is another name for the same inode. Opening either name for an in-place write changes the same file. Permissions belong to that inode too; changing its mode through one checkout changes what the other sees.
Removing a destination name is different. Unlinking a hardlink leaves the source name intact. A check that only deletes or replaces destination files can therefore look safe while missing in-place writes.
The original success test asserted that the seeded file had the same inode as the source. That verified the optimization worked. It did not verify checkout isolation. In fact, it asserted the shared-state property the repair needed to remove.
Matching lockfiles did not help. They establish that the checkouts request the same dependency resolution, not that the installed files will never be modified.
Reproduce the write, not just the layout
The failure was reproduced in temporary directories using the production seeding helper. No live dependency files needed to be touched.
The essential filesystem behavior also fits in a small Python experiment:
from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
with TemporaryDirectory() as directory:
root = Path(directory)
source = root / "source"
source.mkdir()
(source / "dependency.js").write_text("original\n")
destination = root / "worktree"
subprocess.run(["cp", "-al", str(source), str(destination)], check=True)
(destination / "dependency.js").write_text("changed in worktree\n")
print((source / "dependency.js").read_text(), end="")
On this Linux host, it prints changed in worktree.
The new regression tests exercise the production preparation path, then mutate the destination: one writes file contents, another changes permissions. The source must remain unchanged in both cases. Both tests failed before the repair.
Independent files, with sharing below the file boundary
The repair replaces hardlink seeding with:
cp -a --reflink=auto SOURCE_NODE_MODULES DESTINATION_NODE_MODULES
Where supported, a reflink allows separate files to share data blocks until a write requires a private copy. Otherwise this command falls back to an ordinary copy. Either way, the checkouts do not share the same mutable inode.
The surrounding contract stays small: matching lockfiles permit seeding; missing prerequisites fall back to installation. A failed copy removes the partial destination before installation. A separate test checks that package executable symlinks survive the copy.
Making the hardlinks read-only would not have restored isolation. The permission change itself would affect the source, and an owner could change it back. Independent files are the simpler contract.
There is a real cost here. A forced --reflink=always probe on this host failed with Operation not permitted. The automatic command succeeded, but that is not evidence of copy-on-write savings. We kept mutation isolation and gave up the original space-saving guarantee where reflinks are unavailable.
All 37 worktree-creator tests pass after the repair. The helper applies to newly prepared worktrees; it does not retroactively separate files in existing ones, and the repair did not audit their mutation history.
For a worktree bootstrapper, the useful acceptance test is a mutation: write through the new checkout, change a mode, remove a file, and check what happened to the source. Directory structure and inode counts describe the mechanism. The other checkout staying unchanged is the outcome.