Cleaning up stale worktrees safely Jump to heading
Worktrees are easy to create and easy to forget, which is a combination that produces a list of eleven directories, four of which no longer exist on disk and two of which contain work somebody meant to come back to. The cleanup is not difficult, but it has one irreversible step β removing a worktree with uncommitted changes discards files that were never in the object database, and no recovery technique brings those back. This recipe sorts the safe cases from the dangerous ones, within Git worktrees and parallel development.
When to use this approach Jump to heading
git worktree listreturns more entries than you can account for.- Branches refuse to be checked out because a worktree you deleted still claims them.
- Disk usage on a development machine has grown unexpectedly.
- You are about to reclone and want to know what you would lose.
- If you have two worktrees and you created both this morning, there is nothing here for you.
Step 1 β Inventory, including the ones that no longer exist Jump to heading
# Human-readable, with the branch each one claims
git worktree list # Machine-readable, which is what a cleanup script should parse
git worktree list --porcelain | awk '
/^worktree /{p=$2}
/^branch /{b=$2}
/^detached/{b="(detached)"}
/^prunable/{print "PRUNABLE " p " " b; p=b=""}
/^$/{if (p) print "LIVE " p " " b; p=b=""}' # Verification: prunable entries are those whose directory is gone
git worktree list --porcelain | grep -c prunable Step 2 β Prune the administrative leftovers Jump to heading
An entry whose directory has been deleted holds nothing. Pruning it releases the branch it was claiming.
git worktree prune --verbose --dry-run # see what would go
git worktree prune --verbose # then do it # Verification: the branch that was locked is now checkoutable
git worktree list | wc -l
git switch fix/old-thing && git switch - Pruning never touches a directory that exists, so it cannot lose anything. Run it first and the remaining list is shorter and easier to reason about.
Step 3 β Sort the live ones by what they would cost to lose Jump to heading
# Uncommitted changes, per worktree
git worktree list --porcelain | awk '/^worktree /{print $2}' | while read -r wt; do
n=$(git -C "$wt" status --porcelain 2>/dev/null | wc -l)
printf '%3s changed files %s\n' "$n" "$wt"
done # Commits present in a worktree's branch but nowhere else
git worktree list --porcelain | awk '/^worktree /{print $2}' | while read -r wt; do
b=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null) || continue
n=$(git rev-list --count "origin/main..$b" 2>/dev/null || echo 0)
printf '%3s unmerged commits %s (%s)\n' "$n" "$wt" "$b"
done Two numbers, two different risks: uncommitted files are unrecoverable once removed, while unmerged commits survive in the object database and can be recovered through the reflog for a while β see recovering lost commits with git reflog.
Step 4 β Remove the empty ones, commit the rest Jump to heading
# A clean worktree removes without argument
git worktree remove ../app-review # A dirty one refuses, which is correct β deal with it deliberately
git -C ../app-experiment status --short
git -C ../app-experiment stash push -u -m "wip from app-experiment" # or commit it
git worktree remove ../app-experiment # Verification: the stash survives in the shared object store
git stash list | head -3 SAFETY WARNING β
git worktree remove --forcedeletes uncommitted and untracked files immediately. There is no reflog for content that was never committed and no undo. If a worktree refuses to be removed, that refusal is the last safeguard between you and losing someoneβs afternoon; read the status output before overriding it.
Step 5 β Make the cleanup a habit rather than an event Jump to heading
# A weekly one-liner: show worktrees whose branch is already merged
git worktree list --porcelain | awk '/^branch /{print $2}' | sed 's|refs/heads/||' |
while read -r b; do
git merge-base --is-ancestor "$b" origin/main 2>/dev/null \
&& echo "merged, removable: $b"
done # Verification: after the sweep, the list should be short enough to read at a glance
git worktree list | wc -l A worktree whose branch is fully merged contains nothing unique by definition, so it is the safe half of the list. Running this weekly keeps the inventory small enough that the dangerous half stays visible.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why does a branch stay locked after I delete its directory? Jump to heading
Because Git tracks the association in administrative files inside the repository, not in the directory you deleted. git worktree prune clears those, which is the only step needed β there is nothing else to clean up.
Can I move a worktree instead of removing it? Jump to heading
Yes: git worktree move <path> <new-path> updates the bookkeeping along with the directory. Moving it by hand leaves Git pointing at the old location, which produces a prunable entry and a directory Git no longer knows about.
What about a worktree on a disk that is currently unplugged? Jump to heading
Lock it with git worktree lock --reason 'on the external drive'. Without the lock, a prune while the volume is absent removes the administrative entry, and reconnecting the disk leaves an orphaned directory that Git no longer associates with the repository.
Related Jump to heading
- Git Worktrees & Parallel Development β the parent topic and the sharing model.
- Recovering Lost Commits With git reflog β what recovery is possible for committed work.
- Closing Stale Branches and Pull Requests β the same reachability question on the server side.