Skip to content

Git Worktree Remove vs Prune: Clean Up Old Worktrees

How git worktree remove and prune differ, how to remove all worktrees safely, delete their branches, and get back the disk space AI coding agents use.

Updated · 8 min read

Use git worktree remove <path> to delete a linked worktree that still exists on disk: it removes the folder and Git’s record of it, and refuses if there are uncommitted changes. Use git worktree prune only when the folder is already gone. Neither command deletes the branch, so remove that afterward with git branch -d.

TL;DR

  • See every worktree first: git worktree list. Entries marked prunable have lost their folder; entries marked locked are protected.
  • Remove one that still exists: git worktree remove path/to/worktree. Add --force to throw away uncommitted changes.
  • Clean up records for folders you already deleted: git worktree prune --dry-run --verbose, then git worktree prune.
  • Delete the branch separately: git branch -d branch-name.
  • Check each worktree for uncommitted and unpushed work before removing it, and measure it with du -sh. Most of the space is usually node_modules and build output.

Why you suddenly have dozens of worktrees

A worktree is a second (or tenth) checkout of the same repository, on its own branch, sharing one .git folder. That makes it the natural way to run AI coding agents in parallel: each agent gets its own folder and can’t overwrite another agent’s edits.

The tools create them for you, often somewhere you never look:

  • Claude Code puts them under .claude/worktrees/<name>/ at the repository root when you run claude --worktree, or when a subagent runs with worktree isolation. It removes clean ones when a session ends, but keeps any with changes, and leaves worktrees from non-interactive claude -p runs in place.
  • The Codex app creates them in $CODEX_HOME/worktrees and, by default, keeps the 15 most recent.
  • Cursor caps worktrees at 25 per machine by default in recent versions and cleans up older ones on a schedule.

Each worktree has its own copy of the source files. And because it’s a fresh checkout, every agent that runs npm install, pnpm install or a build creates its own node_modules, .next, dist or target folder. Ten agent worktrees of a typical web app can mean ten copies of node_modules.

How to list your git worktrees

Run this from anywhere inside the repository:

git worktree list

The first line is your main worktree. Every line after it is a linked worktree, with its commit and branch. Two flags are worth knowing:

  • prunable: the folder no longer exists (someone ran rm -rf, or a tool deleted it). Git still has a record of it in .git/worktrees/.
  • locked: the worktree is protected from prune and from remove without double force. Agents lock worktrees while they’re working in them.

For output a script can parse, use --porcelain:

git worktree list --porcelain

Each worktree becomes a block with worktree <path>, HEAD <sha>, branch refs/heads/<name> (or detached), and optional locked and prunable lines.

To find every repository on your Mac that has linked worktrees, search for the .git/worktrees folders Git creates:

find ~ -maxdepth 6 -type d -path '*/.git/worktrees' 2>/dev/null

Check for uncommitted and unpushed work first

git worktree remove protects you from one mistake: it refuses to delete a worktree with modified tracked files or untracked files. It doesn’t know about commits that exist only on your machine. Check both before you clean up:

# Uncommitted or untracked changes (empty output means clean)
git -C path/to/worktree status --short

# Commits that aren't on any remote (empty output means everything is pushed)
git -C path/to/worktree log --oneline HEAD --not --remotes

Unpushed commits on a named branch aren’t lost when you remove the worktree, because the branch stays. They are at risk in two cases: you also delete the branch with git branch -D, or the worktree was on a detached HEAD, which agents sometimes use. Commits made on a detached HEAD are only reachable through the reflog after the worktree is gone, and Git’s garbage collection eventually deletes them. If git worktree list shows (detached HEAD) and the log above isn’t empty, create a branch first:

git -C path/to/worktree switch -c rescue-agent-work

How much space are your worktrees using?

The shared .git folder means a worktree doesn’t duplicate your history, but everything else in it is a full copy. Measure each linked worktree:

git worktree list --porcelain | sed -n 's/^worktree //p' | tail -n +2 |
  while IFS= read -r wt; do du -sh "$wt" 2>/dev/null; done

Then see how much of that is dependencies. For Claude Code worktrees:

du -sh .claude/worktrees/*/node_modules 2>/dev/null

A few things du doesn’t tell you:

  • pnpm hard-links packages from a shared store, so deleting one worktree’s node_modules frees less than du reports for it. The files stay in the store until no project uses them.
  • APFS clones (files copied with cp -c or by Finder) share blocks with the original. They look like full copies to du, but deleting one frees almost nothing.
  • Xcode keeps a separate DerivedData folder for every project path, so each worktree you open in Xcode gets its own folder under ~/Library/Developer/Xcode/DerivedData. Removing the worktree doesn’t delete it. See how to delete Xcode DerivedData.

How to remove a git worktree

Once a worktree is clean and its work is pushed or merged:

git worktree remove .claude/worktrees/feature-auth

This deletes the folder (including ignored files such as node_modules and build output) and its record in .git/worktrees/. You can’t remove the main worktree this way.

If Git refuses because of local changes you don’t want:

git worktree remove --force .claude/worktrees/feature-auth

--force permanently discards modified and untracked files in that worktree. It also allows removing worktrees that contain submodules.

How to remove a locked worktree

A locked worktree needs either an unlock or double force:

git worktree unlock .claude/worktrees/feature-auth
git worktree remove .claude/worktrees/feature-auth

# or in one step
git worktree remove --force --force .claude/worktrees/feature-auth

Locks usually mean something. Claude Code holds a lock while an agent or background session runs in a worktree, and you may have locked a worktree on an external drive yourself. Make sure nothing is running there first. git worktree list --verbose shows the lock reason when one was given.

git worktree remove vs prune

Command Use it when What it deletes Touches your files?
git worktree remove <path> The worktree folder still exists The folder and its .git/worktrees/ record Yes, the whole folder
git worktree remove --force <path> The worktree has changes you want to discard Same, including uncommitted work Yes
git worktree prune The folder is already gone (prunable) Only stale records in .git/worktrees/ No
git branch -d <branch> After removing the worktree The branch, if it’s merged No

Preview a prune before running it:

git worktree prune --dry-run --verbose
git worktree prune

Prune skips locked worktrees. You rarely need it for disk space, because the records are tiny. It matters because a stale record keeps the branch “checked out”, which blocks you from deleting or checking out that branch elsewhere. git gc also prunes stale records on its own after three months by default (the gc.worktreePruneExpire setting).

How to remove a worktree and delete its branch

Git won’t delete a branch that’s checked out in any worktree. In git branch output those branches are marked with a +. So remove the worktree first, then the branch:

git worktree remove ../myapp-feature-auth
git branch -d feature-auth

-d refuses if the branch isn’t merged into its upstream (or into your current branch, if it has no upstream). That’s the safety net you want for agent branches. Use -D only for experiments you’ve decided to throw away. The remote branch, if there is one, stays until you delete it with git push origin --delete feature-auth.

How to remove all git worktrees at once

There’s no built-in “remove all” command, but a short loop does it. Run the dry run first. It prints what would happen without deleting anything:

git worktree list --porcelain | sed -n 's/^worktree //p' | tail -n +2 |
  while IFS= read -r wt; do
    if [ ! -d "$wt" ]; then echo "missing (prune): $wt"; continue; fi
    dirty=$(git -C "$wt" status --porcelain)
    unpushed=$(git -C "$wt" log --oneline HEAD --not --remotes)
    if [ -z "$dirty" ] && [ -z "$unpushed" ]; then echo "would remove: $wt"
    else echo "KEEP (uncommitted or unpushed): $wt"; fi
  done

tail -n +2 skips the first entry, your main worktree. If the output looks right, run the same loop with the real command, then prune the leftovers:

git worktree list --porcelain | sed -n 's/^worktree //p' | tail -n +2 |
  while IFS= read -r wt; do
    [ -d "$wt" ] || continue
    if [ -z "$(git -C "$wt" status --porcelain)" ] && [ -z "$(git -C "$wt" log --oneline HEAD --not --remotes)" ]; then
      git worktree remove "$wt" && echo "removed: $wt"
    fi
  done
git worktree prune

The loop never forces anything. Locked worktrees fail with an error and stay, and anything with uncommitted or unpushed work is skipped. Deal with those by hand.

Repositories without a remote are a special case: every commit counts as “unpushed”, so the loop keeps everything. That’s intentional. Check those by hand too.

What about worktrees you deleted with rm -rf?

That’s the most common mess. The folder is gone and the space is back, but git worktree list shows the entry as prunable, and its branch still counts as checked out. Run git worktree prune, then delete the branch if you don’t need it.

If you moved a worktree folder instead of deleting it, don’t prune. Run git worktree repair <new-path> from the main worktree to reconnect it.

Do it in one click with DevCleanup

DevCleanup’s Worktrees screen runs these checks for every repository in your project folders and lists each linked worktree with its branch, last commit date and real size. Worktrees with uncommitted changes, unpushed commits, no remote or a lock get a warning badge and aren’t pre-selected. Clean ones are marked safe to remove, and missing folders are cleaned up with git worktree prune. Removal goes through git worktree remove, never a raw delete. Branches are kept, and every removal is logged in History. Sizes use real-space accounting, so pnpm hard links and APFS clones aren’t counted twice. With Pro, an automatic cleanup rule can remove clean worktrees whose branch is already merged. Pair it with the Projects screen for leftover node_modules in your main checkouts. Download DevCleanup: scanning is free, and Pro ($29 one-time) unlocks the developer cleanup actions.

Frequently asked questions

What is the difference between git worktree remove and git worktree prune?

git worktree remove deletes a linked worktree's folder and its bookkeeping in .git/worktrees. git worktree prune deletes only the bookkeeping for worktrees whose folders are already gone. Use remove for worktrees that still exist, and prune after someone deleted a folder by hand.

Does git worktree remove delete the branch?

No. The branch and all of its commits stay in the repository. Once the worktree is gone, delete the branch separately with git branch -d, or git branch -D if it was never merged.

Can I just delete a worktree folder with rm -rf?

It frees the space, but Git keeps the worktree's metadata and lists it as prunable until you run git worktree prune or Git's garbage collection expires it. You also skip Git's check for uncommitted changes, so git worktree remove is the safer habit.

How do I remove a locked git worktree?

Run git worktree unlock <path> and then git worktree remove <path>, or pass --force twice to remove it in one step. Check first that no agent or script is still using it, because tools such as Claude Code lock worktrees while an agent runs in them.

Does removing a worktree delete its node_modules?

Yes. git worktree remove deletes the whole worktree folder, including ignored files such as node_modules and build output. Caches stored outside the folder, such as Xcode's DerivedData, are not touched.