Worktrees and node_modules: the real cost, and how to avoid it

The only genuine objection to worktrees is dependency directories. It is a real cost, it is almost entirely solvable, and the most obvious solution is the one that will bite you.

By the Continuum team. We build a workbench that runs Claude Code, Codex, and their peers, so the model rates quoted here are the ones our own cost analytics ship with.

The short version

A git worktree checks out tracked files only, so node_modules, .env, and build output do not come across. Install per worktree. To make that nearly free, use a package manager with a content-addressed store: pnpm hard-links from one global store, and bun uses clonefile on macOS and hardlink on Linux. To carry the gitignored config files automatically, use a .worktreeinclude file if Claude Code creates your worktrees, git.worktreeIncludeFiles if VS Code does, or three lines of shell if you do. Do not symlink a shared node_modules.

What you need to know
  • Worktrees carry tracked files only. Everything gitignored is absent by design.
  • pnpm is the highest-leverage fix: one store, hard-linked into every worktree.
  • The store must be on the same filesystem, or pnpm copies instead of linking.
  • .worktreeinclude (Claude Code) and git.worktreeIncludeFiles (VS Code) copy your .env for you.
  • Symlinking one shared node_modules fails silently and later. Do not.
  • Rust, Python, and Go already share their caches. Only the build output is per worktree.

What a new worktree does not have

A worktree is a checkout, and git can only check out what git is tracking. Everything in your .gitignore is, by definition, not tracked, so it is not there. Nothing is broken; the directory is exactly as complete as a fresh clone would be.

A fresh worktree has none of these, and each has a different fix.

MissingWhyFix
node_modules/gitignoredInstall per worktree, from a shared store
.env, .env.localgitignoredCopy on create (see below)
Build output (dist/, .next/)gitignoredRebuild. It is per branch anyway
.venv/, target/, vendor/gitignoredRecreate; the package caches are shared
Editor local settingsUsually gitignoredCopy if you care about them
Uncommitted changesNot committedNothing. That is the isolation you asked for
See exactly what your main checkout has that a worktree will not.
# every ignored path in the current checkout
git status --ignored --short | grep '^!!'

# what that costs, biggest first
git status --ignored --porcelain | sed -n 's/^!! //p' | xargs -I{} du -sh {} 2>/dev/null | sort -rh | head

The source checkout is not the cost

Git objects are shared across every worktree of a repository. Adding a worktree adds one checkout of the tracked files and a few kilobytes of administrative state, not a second copy of your history. On a repository with a long history that difference is enormous, and it is why git worktree add returns in under a second where git clone takes a minute.

Split the two numbers on your own repository.
# shared: history, objects, packs. Paid once, whatever the worktree count.
du -sh .git

# per worktree: tracked files only
du -sh --exclude=.git --exclude=node_modules .

# per worktree: the number that actually matters
du -sh node_modules

The honest arithmetic is that N worktrees cost one repository, N source checkouts, and N dependency trees. The first is free, the second is usually trivial, and the third is the entire objection. Everything below is about collapsing the third.

pnpm: one store, hard-linked everywhere

pnpm keeps every version of every package once, in a content-addressable store. In pnpm's own words, "all the files are saved in a single place on the disk. When packages are installed, their files are hard-linked from that single place, consuming no additional disk space." A second, third, and tenth worktree install therefore add close to nothing.

npm install -g pnpm

# in each worktree
pnpm install

# where the shared store is
pnpm store path

# reclaim space from versions nothing references any more
pnpm store prune

# check the store has not been corrupted by an editor or a build tool
pnpm store status

Default pnpm store location, as documented in August 2026.

PlatformStore path
Linux~/.local/share/pnpm/store
macOS~/Library/pnpm/store
Windows~/AppData/Local/pnpm/store
Adding a worktree costs nothing in git objects and little in source files; the dependency tree is the whole cost, and pnpm collapses it to hard links against one store what a third worktree actually adds .git objects paid once, however many worktrees tracked source one checkout each, usually small node_modules npm a full extract per worktree 420 MB 420 MB 420 MB 1.26 GB in total pnpm hard links to one store one store · 420 MB Hard links need one filesystem: a store on another volume quietly becomes copies.

How each package manager behaves

What a second worktree install actually does on disk.

ManagerMechanismSecond worktree costs
npmTarball cache, full extract per projectA full copy, every time
yarn (classic)Tarball cache, full extract per projectA full copy, faster than npm
yarn (PnP)Zip archives, no node_modules at allAlmost nothing, if your tooling supports PnP
pnpmContent-addressed store, hard linksClose to zero, same filesystem
bunclonefile on macOS, hardlink on LinuxClose to zero, with a copy fallback

bun documents the same idea from the other direction: it "uses the fastest installation method available on the target platform: clonefile on macOS and hardlink on Linux", falling back to copyfile where neither is available. Copy-on-write and hard links solve the same problem, so the practical answer is: if the project is already on npm and you have started running worktrees, moving to pnpm or bun is the change that pays for itself.

Copy the gitignored config automatically

Forgetting .env is the single most common way a new worktree looks broken. The application starts, fails to reach a database, and the failure has nothing to do with worktrees. Three tools now do this copy for you, and they all take the same shape: a list of gitignored paths to bring across.

Claude Code: .worktreeinclude

Put a .worktreeinclude file in the project root. It uses .gitignore syntax, and only files that match a pattern and are gitignored are copied, so tracked files are never duplicated. It applies to every worktree Claude Code creates with git, including subagent worktrees and desktop parallel sessions.

.worktreeinclude
.env
.env.local
config/secrets.json

VS Code: git.worktreeIncludeFiles

The same idea as a setting, applied to worktrees created from the Source Control Repositories view. It takes glob patterns.

.vscode/settings.json
{
  "git.worktreeIncludeFiles": [
    ".env",
    "node_modules/**"
  ]
}

Everyone else: three lines of shell

for f in .env .env.local .env.development; do
  [ -f "$f" ] && cp "$f" "$dir/"
done

Symlinking, and exactly when it breaks

Tempting. Occasionally correct. Usually a trap.
ln -s ../app/node_modules ../app-auth/node_modules

When a shared node_modules survives and when it does not.

SituationVerdict
Identical dependencies, identical versionsWorks, until one branch changes
A branch adds or bumps a dependencyBreaks. Two lockfiles, one tree
Native modules, different Node versionsBreaks confusingly. ABI mismatch
Tooling that writes into node_modulesCross-contaminates. Prisma, Next, .bin shims
Monorepo with workspace linksDo not. The links point at the other worktree
Two agents installing at onceDo not. Concurrent writes to one tree

The same question in other ecosystems

Node is the loud case because node_modules is enormous and per project. Most other toolchains already put the expensive part in a machine-wide cache, so a worktree costs you the build output and nothing else.

EcosystemShared alreadyPer worktree
RustRegistry and sources under ~/.cargotarget/
GoModule cache and build cacheNothing meaningful
Python (uv)Global cache at ~/.cache/uv.venv/, linked from the cache
Python (pip)Wheel cache.venv/, extracted per environment
Java (Gradle, Maven)~/.gradle, ~/.m2Build directories

uv has the same same-filesystem condition pnpm does, and states it directly: "It is important for performance for the cache directory to be located on the same file system as the Python environment uv is operating on. Otherwise, uv will not be able to link files from the cache into the environment and will instead need to fallback to slow copy operations."

Automate it once and stop thinking about it

Worktrees are only worth it if creating one is free. If it takes four commands and a two-minute install, you will stop doing it inside a week and go back to one directory and one agent.

~/bin/wt - a worktree you can actually run in.
#!/usr/bin/env bash
# wt <branch> - create a worktree, carry the config, install.
set -euo pipefail

name="$1"
repo="$(basename "$PWD")"
dir="../${repo}-${name//\//-}"

git worktree add -b "$name" "$dir"

for f in .env .env.local .env.development; do
  [ -f "$f" ] && cp "$f" "$dir/"
done

cd "$dir"
if   [ -f pnpm-lock.yaml ];    then pnpm install --prefer-offline
elif [ -f bun.lock ] || [ -f bun.lockb ]; then bun install
elif [ -f yarn.lock ];         then yarn install
elif [ -f package-lock.json ]; then npm ci
fi

echo "ready: $dir"

Questions people ask

Because a worktree checks out tracked files only, and node_modules is gitignored, so git does not have it to check out. Run your install command in the new worktree, or use a package manager with a shared content-addressed store so that install is nearly free.

With pnpm, effectively yes: one global store hard-linked into each worktree, costing almost no extra disk. Symlinking a single node_modules directory is a different thing and only works when the branches have identical dependencies, failing silently when they do not.

One shared repository, one source checkout per worktree, and one dependency tree per worktree. The first two are cheap because git objects are shared. The third is the whole cost, which is why the package manager decides the answer.

With Claude Code, add a .worktreeinclude file listing the paths; it uses gitignore syntax and only copies files that are both matched and gitignored. In VS Code, set git.worktreeIncludeFiles to a list of globs. Otherwise, copy them in your worktree-creation script.

If you run several worktrees regularly, it is the single highest-leverage change available, because it turns the main objection into a rounding error. Switch the package manager first and confirm CI is green before you start creating worktrees.

Almost always because the store is on a different filesystem from the worktrees. Hard links only work within one filesystem, so pnpm falls back to copying. Check pnpm store path and make sure it is on the same volume as the checkouts.

Create the venv per worktree; uv links it from a global cache when the cache is on the same filesystem. Keep target/ per worktree too: cargo locks the build directory, so one shared CARGO_TARGET_DIR makes parallel builds queue behind each other.

Yes. Commits, branches, tags, and remotes are shared, which is why adding a worktree is close to instant while cloning is not. Only working files, HEAD, and the index are per worktree.

Sources

Every figure above was read from these pages on August 2026. Vendors reprice without notice; if you find a stale number, tell us.

  1. git-worktree manual
  2. pnpm: store settings
  3. Claude Code: run parallel sessions with worktrees
  4. VS Code: branches and worktrees
Try it

Setup,
automatic.

Continuum runs your repository setup command when it creates a session worktree, so a new one is ready to go.

free app · your subscriptions · local-first