Skip to content

Fix: Git Worktree Not Working — Branch Already Checked Out, Prune, Submodules, and Locked Worktrees

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix git worktree errors — fatal: 'branch' is already checked out at, worktree prune removing valid trees, detached HEAD on add, submodules not initialized, moving/locking worktrees, and ignoring per-worktree files.

The Error

You try to add a worktree for a branch and git refuses:

$ git worktree add ../feature-x feature-x
fatal: 'feature-x' is already checked out at '/path/to/main/repo'

Or you create a worktree and end up on a detached HEAD:

$ git worktree add ../experiment
Preparing worktree (detached HEAD abc1234)
HEAD is now at abc1234 chore: bump deps

Or git worktree prune deletes worktrees you still need:

$ git worktree list
/path/to/main          abc1234 [main]
/path/to/feature-x     def5678 [feature-x]

$ git worktree prune
$ git worktree list
/path/to/main          abc1234 [main]
# feature-x gone — but the directory still exists with uncommitted work!

Or submodules show up empty in the new worktree:

$ git worktree add ../feature-x feature-x
$ cd ../feature-x/vendor/lib
$ ls
# Empty.

Why This Happens

A worktree is a second working directory linked to the same .git repository. The main repo holds the objects database and the worktree-specific metadata sits in .git/worktrees/<name>/. Most failures come from:

  • Branches are exclusive across worktrees. A branch can only be checked out in one place at a time. The main repo and every worktree must each be on a different branch (or one of them on a detached HEAD).
  • worktree prune removes “stale” worktrees. A worktree is “stale” if its directory no longer exists at the recorded path. If you moved the directory by hand (without git worktree move), git thinks it’s gone and prunes the metadata.
  • Submodules are per-checkout. A new worktree doesn’t auto-init submodules. You need git submodule update --init --recursive in each worktree.
  • gitignore is global but per-worktree state isn’t. Files like .env need to exist in each worktree if your build expects them.

The mental model that helps: a worktree is a checkout, not a clone. The object database (.git/objects), the ref database (.git/refs or the packed refs file), and the config (.git/config) are all shared. What’s per-worktree is the index, the HEAD pointer, the reflog of that HEAD, and the on-disk working files. When two worktrees seem to disagree about state, almost always the disagreement is about which one’s HEAD or index is current — not about the underlying objects.

A second pattern worth internalizing: the .git entry inside a non-main worktree is a file, not a directory. It contains a single line like gitdir: /path/to/main/.git/worktrees/feature-x. If that path becomes wrong (because you copied, renamed, or moved the worktree by hand), every git command run inside fails with fatal: not a git repository. The fix is always git worktree repair, which rewrites both the .git file and the matching entry in .git/worktrees/<name>/gitdir to point at the actual filesystem locations.

Version History That Changes the Failure Mode

Worktrees have been in git since 2015 but the implementation has been steadily refined. Knowing which git version a colleague (or a stale tutorial) wrote against helps explain why something that “always worked” suddenly doesn’t:

  • Git 2.5 (Jul 2015): Worktrees introduced under the git worktree command. Initially required manual cleanup; no prune, no move, no lock.
  • Git 2.30 (Dec 2020): git worktree prune gained safer defaults and --expire=<time> to avoid aggressive cleanup. Before 2.30, a transient unmount could cost you a worktree.
  • Git 2.32 (Jun 2021): git worktree add --orphan for creating worktrees on a brand-new branch with no parent. Useful for scratch areas and gh-pages-style layouts.
  • Git 2.36 (Apr 2022): git worktree repair matured to handle moved worktrees, renamed worktrees, and worktrees whose .git link file was overwritten by accident. This is the version where “fix a broken worktree” became a one-command operation.
  • Git 2.40 (Mar 2023): Reftable backend design landed in the source tree (still opt-in). Reftable changes how concurrent worktree updates serialize — heavy users of many worktrees see fewer lock contention errors on the new backend.
  • Git 2.46+ (mid 2024 onward): Reftable backend became selectable per-repo via git init --ref-format=reftable. Worktree creation on reftable repos is noticeably faster on cold-cache machines.

If a colleague says “worktrees are flaky” and they’re running git 2.25 from a long-term-support distro, the answer is to upgrade git before adopting any other practice.

Fix 1: Detach or Use a New Branch

If a branch is checked out in the main repo, you can’t git worktree add it elsewhere. Two solutions:

Switch the main repo to a different branch first:

# In main repo
git switch main

# Now create the worktree
git worktree add ../feature-x feature-x

Create a new branch for the worktree:

git worktree add -b feature-x ../feature-x
# Creates a new branch 'feature-x' from the current HEAD, checked out in the worktree.

To base it off a specific ref:

git worktree add -b feature-x ../feature-x main
# 'feature-x' branches from 'main'.

Pro Tip: Adopt the convention: never check out feature branches in the main repo — keep main checked out there, and use worktrees for every other branch. You’ll stop hitting the “already checked out” error entirely.

Fix 2: Avoid Accidental Detached HEAD

git worktree add ../experiment (no branch given) checks out the current HEAD as detached. Commits there aren’t on any branch and are easy to lose.

Always pass a branch name:

# Existing branch:
git worktree add ../feature-x feature-x

# New branch from current HEAD:
git worktree add -b feature-x ../feature-x

# New branch from a specific ref:
git worktree add -b hotfix ../hotfix origin/release/v2

If you do end up detached and made commits, save them before switching:

# Inside the detached worktree
git switch -c saved-work    # Creates a branch pointing at HEAD.

Fix 3: Move Worktrees With git worktree move, Not mv

Manually moving a worktree directory breaks the link:

mv ../feature-x ../old-feature-x  # WRONG
# Now `git worktree list` shows the old path; prune removes it.

Use the git command:

git worktree move ../feature-x ../old-feature-x

This updates .git/worktrees/feature-x/gitdir to the new path and the linked .git file inside the worktree.

If you already moved manually, recover with git worktree repair:

# After a manual mv
cd ../old-feature-x
git worktree repair
# Or from the main repo:
git worktree repair ../old-feature-x

repair updates the metadata to point at the actual filesystem location. Works for both renamed and moved worktrees.

Fix 4: Don’t Prune Until Listing Looks Right

git worktree prune removes metadata for worktrees whose directories are missing. Before pruning, check the list:

git worktree list
# Look for any entry marked 'prunable':
git worktree list -v

If a real worktree shows as prunable, the directory got renamed or moved. Either git worktree move it back (or to its actual location) or run git worktree repair first.

To prune only worktrees that have been “missing” for a while:

git worktree prune --expire=7.days.ago

Combined with regular --dry-run checks, this is the safe pattern:

git worktree prune --dry-run
# Review the output.
git worktree prune

Common Mistake: Running git gc --aggressive while worktrees hold uncommitted changes or unmerged refs. Modern git is worktree-aware (it considers each worktree’s HEAD and index reachable), but if your worktree directory is renamed and shows as prunable, gc may sweep objects it references. Always check git worktree list -v for prunable entries and run git worktree repair before any aggressive cleanup.

Fix 5: Lock Worktrees on Slow Storage

If a worktree lives on an external drive or network mount that comes and goes, git may prune it when the drive is unmounted. Lock it:

git worktree lock ../on-external-drive --reason "Used for backups; do not prune"

Locked worktrees show with locked in git worktree list -v and survive prune. Unlock when ready:

git worktree unlock ../on-external-drive

Lock is also useful for worktrees that hold long-running test environments — you don’t want a colleague’s git worktree prune killing your in-flight CI run.

Fix 6: Init Submodules in Each Worktree

Submodules aren’t auto-checked-out in new worktrees:

git worktree add ../feature-x feature-x
cd ../feature-x
git submodule update --init --recursive

If you do this often, add a hook or a script:

# scripts/new-worktree.sh
#!/usr/bin/env bash
set -e
git worktree add "../$1" "$1"
cd "../$1"
git submodule update --init --recursive
cp ../main-repo/.env .env  # Copy env if needed.
./scripts/new-worktree.sh feature-x

Note: Submodules added by URL hit the network on each update --init. For frequent worktree creation, set up a local mirror or git config submodule.<name>.url to a local path.

Fix 7: .gitignored Files Don’t Carry Over

.env, node_modules, .venv — anything .gitignored isn’t materialized in new worktrees. The build will fail with “missing dependency” until you re-create or copy them.

A safe pattern:

git worktree add ../feature-x feature-x
cd ../feature-x

# Re-create per-worktree state:
cp ../main-repo/.env.local .env.local
npm install  # or pnpm install / cargo build / etc.

For monorepos with many ignored artifacts, link them once with a script. Don’t symlink node_modules between worktrees if your dependencies vary by branch — the worktree using stale deps will silently use the wrong versions.

Pro Tip: direnv (.envrc) survives across worktrees because the file is committed. Use direnv for per-project env vars instead of un-versioned .env files when possible.

Fix 8: Branch Operations Across Worktrees

A few branch commands respect worktree state:

# Delete a branch — fails if checked out in any worktree:
git branch -d feature-x
# error: cannot delete branch 'feature-x' checked out at '../feature-x'

# Remove the worktree first:
git worktree remove ../feature-x
git branch -d feature-x

# Or delete the worktree and the branch together:
git worktree remove ../feature-x
git branch -D feature-x  # Force delete if not merged.

To rename a branch that’s checked out in a worktree:

# This works — git updates the worktree's HEAD too:
git branch -m old-name new-name

Common Mistake: Editing .git/worktrees/<name>/HEAD directly to “fix” a stuck worktree. This bypasses git’s locking and can corrupt the worktree. Use git worktree remove --force and re-add instead.

Still Not Working?

A few less-obvious failures:

  • fatal: not a git repository inside a worktree. The link file .git inside the worktree directory points at .git/worktrees/<name> in the main repo. If you copied the worktree without git’s help, the path is wrong. Run git worktree repair from the main repo with the worktree’s path.
  • Worktree on a different filesystem fails to checkout. Git uses hardlinks to share objects between worktrees on the same filesystem. Across filesystems, it copies. This is slower but works — disable with git config core.worktreeConfig true and configure per-worktree.
  • git pull in worktree A updates branch checked out in worktree B. That’s correct — both worktrees share the same branch refs. After the pull, worktree B’s working directory may be out of sync. git status will show it. Have B run git pull (or git reset --hard) to catch up.
  • git fetch --all is slow with many worktrees. Worktrees share remotes; the fetch is once per remote, not once per worktree. Slow fetch = network/server, not worktree count.
  • git stash in a worktree applies in a different worktree. Stashes are global, indexed by stash ID. Use git stash list and git stash pop stash@{N} explicitly to apply the right one.
  • pre-commit / husky hooks don’t run. Each worktree has its own .git link, not a .git/hooks directory. Hooks live in the main repo’s .git/hooks. If you set core.hooksPath, make sure it’s an absolute path or all worktrees share the location.
  • Windows: long path errors. Nested node_modules in deep worktree paths exceed Windows MAX_PATH. Enable long paths: git config --system core.longpaths true and the matching Windows registry key.
  • git worktree add is slow on large repos. It checks out every file. For frequent worktree creation, consider git clone --filter=blob:none partial clones, or a sparse-checkout config that limits the file set.
  • git worktree remove refuses with “contains modified or untracked files”. Use --force only after you’ve confirmed there’s nothing valuable. A safer pattern: cd into the worktree, git status to see what’s modified, stash or commit, then remove.
  • IDE indexing rebuilds the entire project for each new worktree. JetBrains and VS Code both treat worktrees as separate projects by default. For monorepo-scale repos, point the IDE at the main repo and use git from the terminal to switch worktrees — or set up a shared index location if your IDE supports it.
  • git worktree add succeeds but the new directory’s branch tracks the wrong upstream. When you create a worktree from a local branch with no upstream set, the worktree inherits no upstream either. Run git push -u origin <branch> from inside the worktree once before the first git pull.
  • Multiple worktrees on a network-mounted filesystem corrupt each other’s indexes. Network filesystems often don’t honor the file locking git relies on. Keep worktrees on local disk; if you must share over a network, mount with strict locking semantics (NFSv4 with nolock=false).

For related git workflow issues, see Git detached HEAD, Git submodule update failed, Git stash pop conflict, and Git fatal not a valid object name.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles