DEV Community

Zenobia Panvelwala
Zenobia Panvelwala

Posted on

Git Worktrees, With and Without Claude Code

I was working on one of my projects and wanted to build two versions of the same feature. I could have cloned the repository twice, but I had recently heard about git worktree and decided to give it a try.

With worktrees, you don't need to switch between branches when you have work to do simultaneously. Each worktree is a separate directory with its own cleanly checked-out branch, while all of them share the same underlying .git history. For example, if you're in the middle of refactoring a complex module and a bug comes up, you don't have to stash or abandon your in-progress work — you can quickly create a worktree, fix the bug there, and clean it up when you're done. For full details, see the official Git documentation: https://git-scm.com/docs/git-worktree

Below are the problems I ran into (plus a few that other developers commonly hit), covered as a comparison of two methods: doing it manually, and doing it with Claude Code.

My test project is a Next.js application with TypeScript and Tailwind CSS.

Round One: The Manual Way

To create a worktree off the main repository, I ran:

git worktree add ../example-app-feature-1 -b feature/feature-1
Enter fullscreen mode Exit fullscreen mode

This creates a new directory next to the main repo, checked out on a brand-new branch called feature/feature-1.

Gotchas...

  1. You can't check out the same branch in two worktrees. Git refuses, because two working trees tracking the same branch would corrupt each other's state. That's why the command above uses -b to create a new branch.
  2. Put the worktree outside your repository. A sibling directory (../example-app-feature-1) is the common convention. It isn't a hard requirement — the path can be anywhere — but never create a worktree inside your main repo, or its files will show up as untracked noise in git status.
  3. A fresh worktree is a clean checkout. Untracked and gitignored files don't come along: no node_modules, no .env. Each worktree needs its own dependency install, which costs time and disk space, and your environment files have to be brought over manually.

Workarounds

A. A shell script to create worktrees

#!/usr/bin/env bash
# new-worktree.sh — create a worktree and make it runnable
set -e

NAME=$1
MAIN_DIR=$(git rev-parse --show-toplevel) # root of the main repo

git worktree add "../$(basename "$MAIN_DIR")-$NAME" -b "feature/$NAME"
cd "../$(basename "$MAIN_DIR")-$NAME"

# Copy env/config files that git ignores
cp "$MAIN_DIR/.env" . 2>/dev/null || true
cp "$MAIN_DIR/.env.local" . 2>/dev/null || true

npm install
echo "Worktree ready: $(pwd)"
Enter fullscreen mode Exit fullscreen mode

Two caveats: npm install still spends minutes duplicating gigabytes of dependencies per worktree, and copied environment files drift — if you rotate a secret in the main repo, every existing worktree silently keeps the stale copy.

B. Symlinks

Instead of copying environment files, you can link back to the ones in the main repository:

ln -s "$MAIN_DIR/.env" .env
ln -s "$MAIN_DIR/.env.local" .env.local
Enter fullscreen mode Exit fullscreen mode

Now there's a single source of truth, so nothing drifts. But this has caveats too: some dotenv loaders and Docker bind mounts don't follow symlinks well, and symlinking cuts both ways — if you want per-worktree config, like a different PORT or database name per tree, a symlink is exactly wrong. In that case, symlink (or copy) the shared .env and create a real, per-worktree .env.local containing just the values that differ, such as the port.

C. pnpm

Symlinks solve config; pnpm solves dependencies. pnpm keeps every package in a single global store and hard-links it into each project's node_modules, so an install in a second worktree mostly just creates links instead of re-downloading. In practice, that turns a multi-minute, multi-gigabyte install into seconds and megabytes. In short: with npm, three worktrees cost you three full node_modules; with pnpm, they share one store.

I found pnpm the most suitable option — it avoided duplicate dependency installs, and a per-worktree .env.local let me run multiple dev servers simultaneously without port collisions.

After finishing the feature and pushing the branch, cleanup takes two steps (in this order — git won't delete a branch that's still checked out in a worktree):

git worktree remove ../example-app-feature-1
git branch -d feature/feature-1
Enter fullscreen mode Exit fullscreen mode

If you ever delete a worktree directory manually with rm -rf instead, git will still think it exists; run git worktree prune to clear the stale registration.

Round Two: The Claude Code Way

Same task, same repo, starting clean:

claude --worktree feature-1
Enter fullscreen mode Exit fullscreen mode

Note that --worktree (or -w) takes a name, not a path. Claude Code creates the worktree itself under .claude/worktrees/ inside your repo, checks out a new branch based on your remote's default branch (origin/HEAD), and starts the session inside it — the create/branch/cd dance from Round One becomes one command.

A worktree is still a fresh checkout, though, so your untracked configuration and secret files don't come along automatically here either. The remedy is a .worktreeinclude file in your project root:

# .worktreeinclude — uses .gitignore pattern syntax

.env
config/secrets.json

# add other config files as needed
Enter fullscreen mode Exit fullscreen mode

This tells Claude Code which gitignored files to copy into each new worktree it creates. Two things worth knowing: it only copies files that are both listed here and gitignored (tracked files come along with the checkout anyway), and the file itself should be committed to the repo — it's the manifest, not the secrets — so it travels to every clone and teammate automatically. Pair it with adding .claude/worktrees/ to your .gitignore so Claude's worktrees don't show up as untracked noise in your main checkout.

When you exit the session, Claude cleans up after itself — with a sensible condition: if the worktree has no commits or uncommitted changes, it's removed automatically; if you did make changes, Claude asks whether to keep or remove it, so you can't lose work by accident.

Gotchas...

  1. .worktreeinclude copies; it doesn't symlink, and it doesn't install. Because it copies environment and secret files, the drift problem from Round One applies: rotate an API key in your main .env and every existing worktree keeps the stale copy. It also does nothing about node_modules, so you still pair it with pnpm or a hook that runs your install command on worktree creation. NOTE: Claude Code's hooks (configured in .claude/settings.json) can run pnpm install automatically when a session starts. There's also a WorktreeCreate hook, but it replaces the default worktree creation entirely — including .worktreeinclude — so you'd have to rebuild that logic yourself. For now, a simple hook or just running pnpm install first is the pragmatic choice; custom hooks deserve their own article.
  2. Worktrees isolate code, not infrastructure. Ports still collide, and a shared local database is still shared — an agent running migrations in one worktree can break the app running in another. The fix for port collisions is the same as before: a per-worktree .env.local.
  3. Review bandwidth becomes the real bottleneck. Spinning up multiple Claude agents on multiple features in parallel is easy; reviewing several branches of AI-written diffs is not. Merge conflicts between two branches you only half-remember reviewing are the worst kind to resolve. Parallelize across independent tasks, and expect two or three simultaneous sessions to be your practical ceiling.
  4. Check in your CLAUDE.md. Each new worktree starts a cold session that doesn't inherit what another session learned. A committed CLAUDE.md travels with every worktree automatically and is the main way to keep parallel sessions consistent.

Round Three: Reusable Commands

To make the parallel workflow repeatable, turn it into custom slash commands. Create a file named .claude/commands/parallel-worktrees.md with the following contents:

---
description: "Set up parallel git worktrees for multiple features"
argument-hint: [feature-1] [feature-2] ...
---

I want to develop these features in parallel using git worktrees: $ARGUMENTS

First, analyze the features and flag any that are likely to touch the
same files — warn me about merge-conflict risk before proceeding.

Then set up the environment. Do NOT implement any features; setup only.

1. Ensure we branch from up-to-date main:
   `git fetch origin && git checkout main && git pull`
2. For each feature, check that no branch or directory with that name
   already exists, then create:
   `git worktree add ../example-app-[feature-name] -b feature/[feature-name]`
3. In each worktree:
   - Symlink the shared env file: `ln -s [main-repo-path]/.env .env`
   - Create a `.env.local` containing a unique `PORT` (3001, 3002, ...)
     so dev servers never collide
   - Run `pnpm install` (shared store, no duplicated downloads)
4. Run `git worktree list` and show me the output to confirm.
5. Print a summary table: worktree path, branch, port — and the exact
   commands to launch a Claude session in each
   (`cd ../example-app-[feature-name] && claude`).
Enter fullscreen mode Exit fullscreen mode

Run it with:

/parallel-worktrees feature-1 feature-2 feature-3
Enter fullscreen mode Exit fullscreen mode

Feel free to adapt this template to your project's needs.

Then, to integrate the finished work, create a second command at .claude/commands/integrate-parallel-work.md:

---
description: Integrate parallel worktree feature branches and clean up
argument-hint: [feature-1] [feature-2] ...
---

I have features developed in parallel worktrees to integrate: $ARGUMENTS

Safety first — before merging anything:
1. For each feature, verify branch feature/[feature-name] exists and run
   `git status` in its worktree (../example-app-[feature-name]).
   If any worktree has uncommitted or untracked changes, STOP and show
   me — do not proceed until I decide what to do with them.
2. Update main: `git fetch origin && git checkout main && git pull`

Integration:
3. Create integration branch from main:
   `git checkout -b integration/parallel-features`
4. Merge each feature/[feature-name] one at a time. After each merge:
   - If there are conflicts, resolve them, then PAUSE and show me the
     conflicted files and your resolution as a diff before continuing.
5. Run the full verification suite: `pnpm install && pnpm build && pnpm test`
   (and lint). If anything fails, stop and show me the failure — do not
   attempt fixes on the integration branch without asking.

Finish (only after I explicitly confirm):
6. Merge integration/parallel-features into main and push.
7. Clean up, in this order:
   - `git worktree remove ../example-app-[feature-name]` for each feature
   - `git branch -d feature/[feature-name]` for each (use -d, never -D)
   - `git branch -d integration/parallel-features`
   - `git worktree prune`
8. Show me final `git worktree list` and `git branch` output to confirm
   everything is clean.
Enter fullscreen mode Exit fullscreen mode

If your repo lives on GitHub (or similar) with a PR-based flow, replace the local merge in steps 6–7 with "push the integration branch and open a PR" — merging to main locally is fine for solo projects, but not for team or production setups.

Run it with:

/integrate-parallel-work feature-1 feature-2 feature-3
Enter fullscreen mode Exit fullscreen mode

Final Word

Which method to use depends on the size of the project and whether it's a solo effort or production team work. Manual worktrees give you full control over base refs, locations, and long-lived trees; Claude Code's managed worktrees remove the ceremony for quick parallel tasks. Either way, sharing a standardized command template with teammates helps avoid human error, streamlines the parallel workflow, and lets developers skip the gotchas and focus on the actual task at hand.

Top comments (0)