Beyond Git Worktree
Git WorktreesLong read

Cleaning Up Stale Worktrees in Long-Running Agent Pipelines

Prevent your agent pipeline from drowning in gigabytes of abandoned Git worktrees.

Contributing Editor · · 14 min read
Cover illustration for “Cleaning Up Stale Worktrees in Long-Running Agent Pipelines”
Git Worktrees · September 10, 2026 · 14 min read · 3,198 words

Git worktrees let a single repository check out multiple branches into multiple directories at once, all sharing one object store underneath. That design makes worktrees the natural isolation unit for agent pipelines: spin up a task, give it its own directory and branch, let it run without touching anyone else's files. The catch is what happens after the task ends. Nobody tears the worktree down, and across weeks of continuous agent runs, that oversight turns into gigabytes of dead weight and hundreds of orphaned branches sitting on disk.

The appeal is straightforward. A developer running agents one at a time, waiting for each to finish before starting the next, throws away a huge share of available throughput, since most of an agent's runtime is spent waiting on I/O, not contending for CPU. Worktrees fix that: five agents can operate on five branches simultaneously, in five separate directories, without one agent's half-finished edit colliding with another's. But every parallel task creates a new worktree, and a pipeline running 50 to 200 tasks a day per agent will generate worktrees faster than any human habit of manual cleanup can keep pace with. One team's audit found the count had climbed to 256 active worktrees before a cleanup pass brought it down to 28, deleting around 700 stale local branches across 46 repositories and reclaiming roughly 27 GB of disk in the process.

None of that is a workflow failure. It is what correct use of worktrees looks like at scale, absent a matching lifecycle for removing them. The rest of this piece works through how staleness accumulates, how to measure it, and the concrete Git commands, scripts, and policies that keep it from becoming a runner-killing problem.

How a worktree becomes stale, the four paths

Staleness has a small number of well-defined causes, and recognizing which one produced a given mess determines which fix applies.

The first and most common is manual deletion. A developer, or a script standing in for one, runs rm -rf ../acme-hotfix instead of git worktree remove. The directory disappears, but Git's internal bookkeeping in .git/worktrees/ doesn't know that. It still lists the worktree as registered, and the branch tied to it stays locked, unable to be checked out elsewhere or cleanly deleted, until someone tells Git the directory is actually gone.

The second path runs through CI. A pipeline run fails, times out, or gets cancelled before it reaches its teardown step. Whatever worktree that run created just sits there, on disk and in Git's records, waiting for a cleanup step that never fires because the run never got that far.

Third: orphaned processes. In enterprise CI setups where rebase or merge commands run inside a worktree, a crashed or hung process can leave behind file handles or lock artifacts that conflict with the next build, even after the worktree directory itself looks empty and inert.

Fourth, and probably the sneakiest: remote branch deletion without local follow-through. A pull request merges, the remote branch gets pruned on the server, but the local worktree and its branch ref never got the memo. The worktree is now pointed at a branch that no longer exists upstream, technically valid, functionally dead weight.

Worth encoding directly into any cleanup script: after running git fetch --prune, if git rev-parse --verify refs/remotes/origin/<branch> fails, the remote is gone and the worktree is a cleanup candidate. That single check catches path four reliably.

Here's the trap that makes all of this worse than it needs to be. Git runs a lightweight prune automatically during certain worktree operations, and that gives a false sense that the system is self-healing. It isn't. That automatic prune clears stale metadata entries only. It does not remove directories, does not delete branches, and does not touch orphaned processes. The registry gets tidier while the actual mess on disk keeps growing.

What the disk and inode cost actually looks like

Every worktree needs a full working directory checkout, even though the object store underneath is shared across all of them. In a monorepo, that means file watchers, test runners, and build tools are all running inside each worktree independently, which multiplies I/O pressure well beyond what the raw byte count suggests.

The disk math gets ugly fast. A 2 GB codebase might produce a worktree that consumes something like 5 GB once dependencies and build artifacts land inside it. Six concurrent agents at that rate means 30-plus GB gone before node_modules directories, virtual environments, and compiled output start piling up inside each one individually. And a stale worktree carries exactly the same footprint as an active one. There's no automatic reclamation when a task finishes, no garbage collection tied to a branch merging. The disk usage just sits there until someone or something removes it.

Inodes are the quieter half of this problem, and the one that catches people off guard. Dependency trees and compiled output produce huge numbers of small files. A filesystem can run out of inodes well before it runs out of raw capacity, and when that happens, the errors that come back don't say "disk full." They're confusing, often unrelated-looking failures that take longer to trace to the real cause.

There's a cognitive cost too, separate from the physical one. Every worktree leaves behind a local branch ref, and after the remote copy gets pruned, that ref shows up in git branch -vv marked [gone]. It stays there until someone explicitly deletes it. A developer running git branch to get their bearings on a repo now has to mentally filter out a wall of dead references before finding what's actually relevant. One practitioner reported a system that had grown to 371 worktrees, an extreme case, but a useful marker of how far this pattern can run ahead of any informal cleanup habit before someone notices.

Detecting what you have before you delete anything

git worktree list is the ground truth here. Not an IDE plugin, not a filesystem browse, the command itself. IDE support for worktrees has been a recent and incomplete addition across tooling: VS Code added worktree support in July 2025, and JetBrains IDEs shipped first-class support in the 2026.1 release in March 2026. Any installation older than those releases will show an incomplete picture of what actually exists, so treating the IDE view as authoritative is a mistake worth avoiding.

For scripting, git worktree list --porcelain is the format to use. It's machine-parseable, and it's the correct input for any automated cleanup tool, rather than trying to regex-parse the human-readable table.

The detection sequence itself is short. Run git fetch --prune to sync remote state first. Then, for each non-primary worktree, check git rev-parse --verify refs/remotes/origin/<branch>. A failure there means the remote branch is gone and the worktree is a candidate for removal. Separately, run git branch -vv and look for [gone] entries, which catches branch refs that survived after their worktree directory was already manually deleted.

Before removing anything, run git -C <path> status to surface uncommitted changes. There is no undo after a forced removal, and skipping this check is how someone loses a day of unpushed work to a cleanup script.

Locked worktrees need separate handling. A worktree with a lock file won't prune automatically, and git worktree list --porcelain surfaces lock status directly in its output. Locked entries need explicit attention before a cleanup pass can proceed past them. Practitioners running agent-heavy repos generally recommend running git worktree list at least weekly, simply to keep the count from growing unnoticed.

The core Git primitives for removal and what each one does

git worktree remove <path> is the standard tool. It removes the working directory and deregisters the worktree from Git's metadata in one step. By default, it refuses to run if there are untracked files or uncommitted modifications to tracked files, which is the safety rail doing its job.

git worktree remove --force overrides that safety check and discards in-progress changes outright. It should always be preceded by a status check, since this operation is irreversible and Git will not ask twice.

git worktree prune is a different operation entirely, and worth not confusing with the one above. It clears stale metadata entries for worktrees whose directories no longer exist on disk (the manual-deletion case from earlier), but it does not remove directories and it does not remove branch refs. It only cleans Git's internal registration. Running it with -v for verbose output is useful in CI logs, since it confirms exactly which entries a script actually touched.

Locked worktrees have their own removal path. git worktree unlock <path> clears the lock and lets subsequent prune, move, or delete operations proceed normally. There's also a shortcut, git worktree remove --force --force, using the flag twice, which removes a locked worktree without unlocking it first. That works as a last resort, but unlocking explicitly first is the cleaner move and leaves a clearer audit trail for anyone debugging the cleanup script later.

One detail that trips up a lot of scripts: pruning does not delete the local branch ref. After removing a worktree, git branch -d <branch> (or -D for a branch that never merged) has to run as a separate step. Removal is only half the job, and skipping the second half is exactly how repositories end up full of [gone] refs.

Some agent tooling handles part of this automatically. Some tooling, for instance, auto-removes a worktree if the agent made no changes during its run. If changes exist, it returns the path and branch for review instead of deleting anything, which is the sensible default: don't destroy work nobody's looked at yet. But it means post-merge cleanup still falls to the pipeline, not the agent. That caution about not deleting unreviewed work doesn't extend to actually completing the cleanup once that review has happened.

A scripted cleanup pattern that is safe to run in CI

A cleanup script that's safe to run unattended follows a fairly standard loop. Parse git worktree list --porcelain to enumerate every non-primary worktree. For each one, check merge status, check for uncommitted changes, and confirm it isn't the primary worktree, skipping anything that fails a safety condition. Run git worktree remove --force on whatever's left, follow with git worktree prune -v to sweep any remaining metadata, then verify the worktree count has dropped back to one (just the primary) before letting the pipeline continue past that step.

The safety conditions matter more than the loop structure. The branch has to be merged into the target base branch before deletion. There must be no uncommitted changes, checked via git -C <path> status --porcelain. The target can't be the primary worktree, and it can't be checked out in any other live session at the same time. Skip any one of these and a cleanup script becomes a data-loss script.

There's also an interactive variant, better suited to human-in-the-loop workflows than fully automated CI. It detects worktrees sitting on branches already merged into main, presents the list to a person, and deletes only what gets explicitly confirmed, skipping main itself, the currently active branch, and anything with uncommitted changes.

After the worktree directory is gone, the script needs to delete the local branch ref too: git branch -d <branch>. Scripts that skip this step leave [gone] refs behind, and those accumulate exactly the way the worktrees themselves did, defeating half the point of running the cleanup at all.

Path hygiene matters more in CI than it might seem. Using CI-provided variables like $CI_PROJECT_DIR or $WORKSPACE for worktree paths, rather than hardcoded absolute paths, keeps the script portable across different runner environments, since a hardcoded path that works on one runner will silently break on the next.

Two anti-patterns deserve explicit guards in the script itself. First, never share a worktree across pipeline runs. Each run should create its own fresh worktree; state contamination between runs produces the same failure modes as sharing a working directory between two agents at once. Second, never skip cleanup on a failed run. Failed pipelines are a major source of stale worktrees, precisely because the teardown step is the one most likely to get skipped when something upstream has already gone wrong. The cleanup step belongs in a finally block, or whatever the pipeline's equivalent construct is, so it runs regardless of how the rest of the job ended.

Timeout mechanisms and disk monitoring as a second line of defense

A cleanup script that runs at the end of a pipeline is no help when the pipeline never reaches its end. A hung task holds its worktree's disk space indefinitely, blocks the runner it's sitting on, and may be holding a lock that prevents any subsequent pipeline from even starting.

Per-worktree timeouts close that gap. If a worktree has been registered longer than the maximum expected duration for its task, a timeout mechanism should trigger a forced removal regardless of what the task thinks it's still doing. In enterprise CI environments, orphaned processes left behind by rebase steps running inside worktrees can interfere with subsequent operations, which is exactly the kind of failure a timeout is meant to catch before it spreads.

Disk monitoring is the second layer. Alerting on runner disk usage before it reaches a threshold that would actually cause build failures is far less disruptive than discovering the problem when a build fails mid-run because it ran out of space. That failure mode is harder to diagnose after the fact than a proactive alert that fires early. And it isn't only about bytes: inode monitoring has to run alongside byte-level disk monitoring, since large dependency trees inside worktrees can exhaust inodes independently of raw capacity. A runner can report plenty of available disk space while being completely unable to create a new file.

Lock file hygiene deserves its own mention here, since it's a narrow problem with an outsized blast radius. A stale .git/index.lock, left behind by a crashed agent process, blocks every subsequent Git operation in that working tree until the lock is removed, either manually or by the next Git command that clears it. In a multi-agent environment, one stale lock file can freeze all progress on a shared repository until a person steps in. Monitoring for lock files older than some threshold age is a cheap, low-effort safeguard against that particular failure mode.

This is also where sandbox design starts to matter. Isolated agent environments, containers or VMs scoped to a single task, naturally contain the blast radius of a stuck worktree to that one disposable environment, instead of a shared runner that every other task also depends on. Cleanup in that model stops being "surgically remove one worktree from a shared filesystem" and becomes "destroy the sandbox," which is a much simpler operation to get right.

Lifecycle policies that prevent accumulation rather than remediate it

Scripts and timeouts are remediation. They clean up a mess after it's already formed. A lifecycle policy is the alternative: define worktree creation and worktree removal as paired operations from the start, specified in the same configuration that defines the agent task itself, rather than left to whatever cleanup discipline a team happens to maintain.

Naming conventions are the foundation this rests on. A schema like [repo-name]-[ticket-id] lets a cleanup script identify a worktree by its associated ticket status, closed or merged, without having to parse branch names ambiguously or guess at intent from a free-text branch title.

The actual trigger points worth defining explicitly: on PR merge, remove the worktree and delete the local branch ref. On PR close without a merge, remove the worktree but consider archiving the branch ref with a prefix like archived/ instead of deleting it outright, preserving it for later audit. On timeout, force-remove any worktree older than the maximum task duration regardless of merge status. On pipeline failure, cleanup runs unconditionally, in a finally equivalent, since failed runs are the most common source of orphaned worktrees to begin with.

There's a structural argument for keeping agent configuration in version-controlled YAML, checked into the repo alongside everything else. When the cleanup policy lives next to the agent task definition, the same review process that approves an agent's behavior also approves its cleanup lifecycle. Cleanup stops being an afterthought bolted onto operations and becomes part of the agent's actual specification, reviewed the same way as everything else about what the agent is allowed to do.

Not every team needs per-task worktree creation at all. Some maintain a fixed set of named, persistent slots, work, review, scratch, that get reused across tasks instead of created and destroyed each time. That bounds the maximum worktree count by construction, at the cost of some isolation between tasks that now share a slot sequentially. It's a reasonable fit for human-driven workflows with modest concurrency. It's a poor fit for high-throughput agent pipelines running dozens of parallel tasks, where reuse just reintroduces the state-contamination problem that per-task worktrees exist to solve in the first place.

The enforcement point matters as much as the policy itself. Cleanup has to be enforced at the infrastructure layer, not left to agent behavior alone. An agent that finishes cleanly and made no changes might auto-remove its own worktree. An agent that crashes, gets interrupted, or hits an unexpected error will not, and infrastructure-level enforcement is the only thing standing between that failure and another slow climb toward hundreds of stale worktrees.

How sandbox isolation changes the cleanup problem at the infrastructure layer

Everything above describes cleanup as a set of operations performed against a shared filesystem: list what's there, check what's stale, remove it carefully, and repeat on a schedule. Sandbox isolation, running each agent task inside its own container or VM rather than a shared runner, changes the nature of the problem rather than just adding another layer of tooling on top of it.

When a task lives inside a disposable sandbox, the worktree inside it never has to be surgically identified and removed from among dozens of siblings on a shared disk. The entire sandbox gets destroyed as a unit once the task ends or times out, and the worktree disappears along with it, no git worktree remove, no separate branch-ref deletion, no risk of leaving a locked or orphaned entry behind on a filesystem other tasks still depend on. Disk exhaustion and inode exhaustion, the two hard limits described earlier, stop being pipeline-wide risks and become bounded to a single environment that was never going to outlive its task anyway.

That doesn't eliminate the need for the lifecycle thinking laid out above. Merge status still has to be checked before a task's changes are considered done, timeouts still matter for tasks that hang, and naming conventions still help trace what a given sandbox was doing. But it does mean the failure modes that make worktree cleanup genuinely hard in a shared-runner environment, a stuck process blocking every other agent, a lock file freezing an entire team's pipeline, an inode count creeping toward a filesystem's ceiling with no clear warning, get scoped down to a single disposable unit instead of a shared resource everyone else is also depending on. The cleanup problem doesn't go away. It just stops being everyone's problem at once.

Sources

  1. Git Worktrees for AI Coding: How to Run Multiple Agents Without Conflicts
  2. [DOCS] Worktree cleanup docs omit killed-agent locked `.git/worktrees/` registration cleanup · Issue #70451 · anthropics/claude-code
  3. Every AI Agent You Add Leaves Something Behind to Clean Up
  4. cleanup-agent-worktree | Skills Marketplace · LobeHub
  5. GitWorktree.org
  6. Git Worktree Cleanup: Remove and Prune Worktrees Safely
  7. git worktree prune: Clean Up Stale Git Worktrees | WorktreeWise
  8. brtkwr.com
Filed underGit Worktrees

More in Git Worktrees