Blog / git
Clean up git worktrees without breaking a running agent
git worktree remove deletes a clean worktree while an agent still works in it. What the agent sees next, a check for running processes, and git worktree lock.
On this page
Running agents in parallel means worktrees: one folder per branch, so two agents never edit the same checkout. After a few days you have a dozen of them, and cleaning up is a loop of git worktree remove. The catch is that git has no idea an agent is still working in one of them.
We tested what happens with git 2.54 on macOS: a process sitting in a worktree, the worktree removed under it, and what that process sees next.
git removes a worktree with a process inside it#
We made a worktree, started a process whose working directory was that worktree, and removed it:
git worktree add ../feat -b feat
(cd ../feat && sleep 30) &
git worktree remove ../featgit worktree remove refuses only when the worktree has changes:
fatal: '../feat' contains modified or untracked files, use --force to delete itWith a clean worktree it succeeded at once, with the process still inside. Most worktrees look clean between an agent's edits, so "git refused, so it must be in use" is not a check you can rely on.
What the agent sees next#
We ran a script inside a worktree, removed the worktree two seconds in, and let the script carry on:
| Command inside the removed folder | Result |
|---|---|
pwd | Still prints the old path |
git status | fatal: Unable to read current working directory: No such file or directory |
echo hi > out.txt | No such file or directory |
node -e 'process.cwd()' | Error: ENOENT: process.cwd failed … the current working directory was likely removed |
npm -v | Fails with the same Node error |
pwd keeps answering because the shell remembers the path, which is what makes this confusing: the agent thinks it's in a folder that is gone, and every git, node or npm command it tries fails. An agent in this state tends to retry, or report errors that look unrelated, until someone restarts it in a real folder.
The branch survives. git branch --list feat still showed it after the removal, so committed work isn't lost. Uncommitted work is: --force deletes the changes along with the folder.
Check for processes before you remove#
lsof can list every process whose working directory is in a folder:
lsof -d cwd | grep "$(cd ../feat && pwd -P)"On our worktree it printed:
sleep 15835 mahmoud cwd DIR 1,17 128 1057634354 /private/tmp/b3-wt/feat3Note the path: on macOS /tmp is really /private/tmp, and lsof prints the real path. pwd -P gives you that real path, so the grep matches. On Linux, lsof works the same way.
A cleanup loop that skips anything in use:
git worktree list --porcelain | sed -n 's/^worktree //p' | tail -n +2 | while read -r wt; do
real=$(cd "$wt" 2>/dev/null && pwd -P) || continue
if lsof -d cwd 2>/dev/null | grep -qE "$real(/|$)"; then
echo "skip (in use): $wt"
else
git worktree remove "$wt" || echo "kept (has changes): $wt"
fi
donetail -n +2 skips the first entry, which is always your main checkout. The (/|$) stops ../feat from matching a process in ../feat2. The loop never passes --force, so a worktree with changes stays and says why. An agent that's waiting for you, with no command running, is still a process with its working directory there, so it shows up too.
Lock the worktrees agents are using#
git has a lock for exactly this:
git worktree lock --reason "agent running" ../featWith the lock in place:
$ git worktree remove ../feat
fatal: cannot remove a locked working tree, lock reason: agent running
use 'remove -f -f' to override or unlock firstA single --force doesn't get past it either; only remove -f -f or git worktree unlock ../feat does. git worktree prune also leaves locked worktrees alone, and git worktree list --porcelain shows a locked agent running line, so anyone looking can see why it's there.
The habit that works: lock a worktree when you start an agent in it, unlock it when the branch is merged, then remove it.
git worktree add ../feat -b feat && git worktree lock --reason "agent running" ../feat
# ...agent works, branch merges...
git worktree unlock ../feat && git worktree remove ../featClaude Code can create a worktree for you with claude --worktree <name>. Whoever creates it, the same rule holds: git only protects it from removal if it's locked or has uncommitted changes.
If it already happened#
The worktree is gone and an agent is stuck in it. Committed work is on the branch, so put the folder back from it:
git worktree add ../feat featWe committed a file in a worktree, removed the worktree, and ran that line. Git answered HEAD is now at ef1e556 agent work, and the file was back. Then restart the agent in the new folder, or tell it to cd there. An agent that only lost its folder picks up from the last commit; anything it hadn't committed is gone and has to be redone.
If someone deleted the folder with rm -r instead of git worktree remove, git still thinks the worktree exists. git worktree list marks it prunable, and adding it again fails:
fatal: '/tmp/b3-wt/r2' is a missing but already registered worktree;
use 'add -f' to override, or 'prune' or 'remove' to cleargit worktree prune clears the stale entry (-v prints gitdir file points to non-existent location for each one it removes), and then git worktree add works again. Locked worktrees are skipped by prune, which is one more reason to lock the ones in use.
The short version#
git worktree removedeletes a clean worktree even while a process is working inside it.- The agent left behind still sees its old path in
pwd, but git, node and npm fail. - Check first with
lsof -d cwd | grep "$(cd <worktree> && pwd -P)". git worktree lock --reason "…"blocksremove,remove -fandpruneuntil you unlock it.- Never script cleanup with
--force: it deletes uncommitted work.
On a team, the worktree an agent is using may belong to a teammate's session on a shared machine, and removing it stops their work, not yours. A locked worktree with a reason is the note that tells them. For setting new worktrees up so the agent can start working at once, see git worktrees, .env and node_modules.