Skip to content

Clean up AI agent worktrees

Codex, Cursor, Conductor and Claude Code can run each task in its own git worktree. A worktree is a full checkout of your repository, and it gets its own node_modules, ios/Pods, ios/build and Android build. For a React Native app that means one to several gigabytes per task. The tools rarely delete these worktrees when the task is done.

lu-cleaner finds these worktrees wherever they are. It shows which tool created each one and what state its git work is in. It removes finished worktrees with git worktree remove, and the branch is always kept.

Terminal window
lu-cleaner worktrees # interactive picker (alias: lu-cleaner wt)
lu-cleaner worktrees --list # the same list as a table
lu-cleaner scan -c worktrees --all # with risk, ★ recommendations and warnings

$ lu-cleaner worktrees --list NAME TOOL BRANCH STATUS SIZE AGE PATH my-app · feature/login codex feature/login merged 2.40 GB 9d /.codex/worktrees/a1b2/my-app my-app · c3d4 codex (detached) clean 1.90 GB 3w /.codex/worktrees/c3d4/my-app shop-app · fix-cart conductor fix-cart dirty 1.20 GB 2d /conductor/workspaces/shop-app/fix-cart ! 3 uncommitted changes · active Conductor workspace — archive it in Conductor instead my-app · worktree-onboarding claude worktree-onboarding unpushed 850 MB 5d /dev/my-app/.claude/worktrees/onboarding ! 2 unpushed commits (branch worktree-onboarding is kept) my-app · prune 1 stale worktree… ? — ~/dev/my-app   Total 6.35 GB in 5 items · ★ 4.30 GB recommended (3 items) Disk: 38.2 GB free of 494 GB (92% used) → run: lu-cleaner clean --smart -c worktrees

Each warning is printed under its row, with details such as the repositories nested in a worktree. The picker shows the same information in the details panel (enter). The size of the prune item is ? because what git worktree prune frees is only known after it runs.

Tool Where its worktrees live Item kind
Codex ~/.codex/worktrees/<id>/<repo> codex-worktree
Cursor ~/.cursor/worktrees/… cursor-worktree
Conductor ~/conductor/workspaces/<repo>/<workspace> conductor-worktree
Claude Code (CLI) <repo>/.claude/worktrees/<name> (created by claude --worktree and subagent isolation), ~/.claude/worktrees, ~/.claude-worktrees claude-worktree
Claude desktop ~/Library/Application Support/Claude/worktrees, and <repo>/.claude/worktrees entries on claude/… branches claude-desktop-worktree
Created by hand Anywhere: every worktree that git worktree list reports for your repositories, plus <repo>/.worktrees, <repo>/worktrees, <repo>-worktrees and ~/.worktrees manual-worktree

lu-cleaner also labels worktrees from Superset (~/.superset/worktrees), Claude Squad, vibe-kanban and Multica. You can use the item kind with --kind, for example -k codex-worktree.

lu-cleaner finds worktrees in four ways, so it also catches worktrees that are not in the usual folders:

  1. It walks the folders listed above, up to four levels deep, plus any folder in worktree_roots in your configuration.
  2. It walks your project roots to find repositories. These are the roots in your config, or folders detected automatically such as ~/dev, ~/Projects and ~/conductor/repos.
  3. It runs git worktree list in every repository it found. This finds that repository's worktrees wherever they are.
  4. It looks inside the usual nested folders (.claude/worktrees, .worktrees, worktrees, <repo>-worktrees).

If several of these methods reach the same worktree, it is counted only once: lu-cleaner compares the folder's device and inode. Submodules, and worktrees of submodules, are not linked worktrees of your repositories and are left out.

Some worktrees are listed but cannot be selected:

  • On an external volume. Removing it frees nothing on your internal disk.
  • Main repository on a volume that is not mounted. Its status is unknown until you plug the volume in.
  • Git data that cannot be read, because of permissions or macOS privacy protection. Its state is unknown, so it is only reported. Fix the access (for example, grant Full Disk Access to your terminal), then scan again.
  • Main repository moved or renamed. Git still tracks the worktree, so it is not an orphan. The warning gives the git -C <new location> worktree repair <path> command that relinks it.
  • Outside your home folder, for example in /tmp. The safety guard only deletes inside your home folder and your per-user temp folder. The item shows the exact git -C … worktree remove … command to run yourself.
  • Your current directory. Run lu-cleaner from another folder.

Each worktree gets one status. When several could apply, the one highest in this table wins:

Status What it means Risk Removed without --force?
orphan Git no longer tracks the folder. Its main repository was deleted, its entry was pruned, or the folder is a copy of another worktree. caution No. With --force, it is deleted as a plain folder: git cannot inspect it.
locked Locked with git worktree lock. Tools often do this while an agent runs. caution No
dirty Has uncommitted changes, including untracked files caution No
unpushed Has commits that no remote contains: ahead of its upstream, or no upstream at all moderate on a branch, caution on a detached HEAD On a branch, yes: the branch keeps the commits. On a detached HEAD, only if another branch, a tag or a remote-tracking branch contains them.
merged Everything is already in the default branch (origin/HEAD, origin/main, origin/master, main or master), including commits that were rebased or cherry-picked onto it moderate Yes
clean No local changes and everything is pushed, but not merged yet moderate Yes
unknown git status failed or timed out; the worktree is on another volume; its git data or main repository cannot be read; its main repository is on an unmounted volume or was moved caution or report-only Only if git can read it at clean time

Some notes on these statuses:

  • Orphan means verifiably gone. A worktree is an orphan only when its git data (and, for a deleted main repository, the repository itself) is missing and the folder above it can be read. When it cannot be read (permissions, macOS privacy protection, unmounted volume), the worktree is unknown and only reported. git worktree prune is never run for such an entry either.
  • Detached HEAD is normal for Codex. Codex often checks out a detached HEAD. That is fine as long as the commit is also on a branch, a tag or a remote: the worktree is then clean or merged. It only becomes a problem when the agent made commits that exist nowhere else.
  • Squash-merged pull requests usually show as unpushed. Suppose a branch with several commits was squash-merged. Its commits don't match the default branch one by one, and once the PR branch is deleted on the remote, the warning says upstream branch deleted. Removal is still safe, because the local branch is kept. Check that the PR is merged, then select the worktree by hand.

Some conditions make a worktree caution whatever its status. Others keep it out of smart select:

Warning Effect
in use by …: a process has its current directory inside (shell, dev server, agent) caution
open in Cursor / open in VS Code: an editor window shows this folder caution
N active Codex threads, active Claude desktop session, active Conductor workspace — archive it in Conductor instead caution
moved: git records it at …: the folder was moved by hand caution. Removal is refused until you run the suggested git worktree repair.
ignored secret/local files would be lost: … caution. Git ignores these files, so git worktree remove deletes them: .env and .env.*, .envrc, .npmrc, keystores, signing keys and certificates, provisioning profiles, google-services.json, GoogleService-Info.plist, *.local.* overrides, settings.local.json, CLAUDE.local.md… A file identical to the same file in the main checkout does not count, nor do templates such as .env.example.
contains another git repository or worktree: … caution. Removal is refused without --force, because git would delete the nested checkout too.
N unpushed commits (branch … is kept) Stays moderate but is never smart-selected
current directory Cannot be selected at all

Smart select picks a worktree only when all of these are true. Smart select is the a key in the picker and --smart on the command line.

  • Its status is clean or merged, so its risk is moderate.
  • It has no warning at all: no unpushed commits, no ignored secret files, no nested repository, and no process, editor window or active agent session using it.
  • It has been idle for at least 7 days, or 1 day if it is merged into the default branch. If your stale_after setting is shorter than 7 days, it applies instead.

"Idle" means the time since the worktree's last activity. lu-cleaner takes the most recent of these events: the worktree's creation, its last commit, changes to its index and HEAD, the modification times of its changed files, and the last activity of the Codex thread or Claude desktop session using it. It ignores the folder's own modification time, because Expo, Xcode and indexers update it even when nobody works there.

Stale worktree entries (see below) are always recommended. Orphans never are.

See Risk levels and smart select for the rules that apply to the other categories.

Remove the worktree, or only its artifacts?

Section titled “Remove the worktree, or only its artifacts?”

Removing a worktree frees everything, but the task's checkout is gone. Removing only its artifacts (node_modules, ios/Pods, native builds) usually frees most of the space and keeps the sources, uncommitted edits, .env files and the tool's link to the folder.

To see which option frees what, check the worktree's details. In the picker, press enter. With --json, read meta. The size is split into node_modules, ios/Pods, ios/build, android/app/build, android/.gradle, android/.cxx, .expo, .next, .turbo, dist, their total artifacts, and the checkout itself.

Situation What to do Why
The task is finished: branch merged or pushed, nothing open in it Remove the worktree Frees the checkout and its artifacts. The branch stays in the repository.
You will come back to the task, or its agent thread or workspace is still active Remove only the artifacts Keeps the sources and the tool's state. A reinstall brings the dependencies back.
The worktree is dirty Remove only the artifacts, or commit or stash first Removing the worktree is refused anyway
It holds git-ignored .env files or keys you still need Copy them out first, or remove only the artifacts git worktree remove deletes ignored files
Detached HEAD with commits that are on no branch Run git branch <name> inside it, then remove it Otherwise those commits become unreachable
It is a Conductor workspace Archive it in Conductor Conductor keeps its archive commit and its own view stays consistent
orphan Look inside first. Delete it with --force if nothing is worth recovering Git cannot tell whether it contains unique work

The artifacts scanner also walks every worktree folder. Each artifact's name starts with the tool whose worktree contains it, for example codex:my-app › node_modules. To see only the artifacts inside one tool's worktrees, open the artifacts picker and filter by that prefix:

Terminal window
lu-cleaner artifacts

Then press / and type the prefix:

Filter Shows the artifacts inside
codex: Codex worktrees
cursor: Cursor worktrees
conductor: Conductor workspaces
claude: .claude/worktrees folders (Claude Code, and Claude desktop worktrees stored there)
claude-desktop: ~/Library/Application Support/Claude/worktrees
worktree: Other linked worktrees: .worktrees folders and checkouts created by hand under your roots

Press a to smart-select them, or space to pick items one by one. Then press d to clean. Smart select never picks artifacts of a project that was active in the last 24 hours. See Keyboard shortcuts for all keys.

You can select a worktree and some artifacts inside it at the same time. The artifacts are then dropped from the plan and nothing is counted twice.

  1. Everything is checked again. Your scan may be minutes old, so lu-cleaner checks the worktree again right before it acts:

    • The folder must be the same folder as during the scan (same inode).
    • It must pass the safety guard.
    • It must not contain your current directory, nor a mounted volume or disk image.
    • No other process may have its current directory in it or run a program from it.
    • No Codex thread, Claude desktop session or Conductor workspace may have started using it since the scan.
    • Git must still record the worktree at this path. A folder that was moved or copied by hand is refused, with the git worktree repair command to run.

    If any check fails, the item is skipped with the reason.

  2. lu-cleaner's own git checks run. Unless you pass --force, removal is refused in these cases:

    • The worktree is locked.
    • It contains another worktree of the same repository, or any other git repository (a folder with a .git entry, or a bare repository), even in an ignored folder. Git would delete it along. Dependency folders such as node_modules and Pods are not searched.
    • HEAD is detached and some of its commits are on no branch, tag or remote-tracking ref.
    • git status reports any change, tracked or untracked. lu-cleaner passes explicit flags, so a status.showUntrackedFiles = no setting in your git config cannot hide untracked files.
    • Its main repository no longer exists, so git cannot look for uncommitted work.
    • lu-cleaner cannot tell whether commits would be lost, or cannot read the git status.

    --dry-run runs these same checks and reports the same refusals.

  3. Git removes the checkout. lu-cleaner runs git worktree remove <path> from the main repository, without --force. Git applies its own refusals as a second line of defense: modified or untracked files, locks, submodules. Ignored files such as node_modules, Pods and .env do not block the removal, and they are deleted with the checkout. Git also removes the worktree's entry in the main repository's .git/worktrees/.

  4. The branch is kept. Its commits stay in the main repository, pushed or not. That's why unpushed commits on a named branch are safe. When you no longer need the branch, delete it yourself, for example with git branch -d feature/login.

  5. The empty task folder is removed. Codex keeps each worktree in a per-task folder, such as ~/.codex/worktrees/a1b2/. If only its marker files (.codex-worktree-name, .DS_Store) are left, lu-cleaner removes that folder too.

  6. The removal is recorded. Worktrees are removed one at a time, after the parallel file deletions. Each removal is added to the history.

When you select a worktree and another worktree nested inside it, both are kept in the plan and the inner one is removed first, with its own checks. If the inner one is not removed, the outer one is kept too.

Worktrees are never moved to the Trash: in Trash mode (--trash, use_trash, or t in the picker), they are skipped with not possible in Trash mode (it would delete permanently).

A worktree folder is sometimes deleted with rm, by Cursor or Conductor, or by macOS cleaning up /tmp. Git still has an entry for it in the main repository, and until that entry is pruned, git refuses to check out or delete its branch elsewhere. lu-cleaner shows one item per repository with such entries, for example my-app · prune 2 stale worktree entries:

  • Risk is safe, and the item is always recommended.
  • Cleaning it runs git -C ~/dev/my-app worktree prune.
  • An entry counts as stale only when its checkout verifiably no longer exists. Some entries may belong to a checkout on an unmounted volume, to a checkout that cannot be read, or to a checkout that was moved. git worktree prune would drop those too, so in that case lu-cleaner only deletes the verified entries' metadata folders instead of running the command.
  • Both variants are verified again right before cleaning.

An orphan is a folder that still has a .git file, but git no longer knows it. Git cannot remove it, and cannot tell whether it contains work that exists nowhere else. So an orphan:

  • is caution and never recommended;
  • is deleted as a plain folder, after the safety guard, only with --force. Without it, the item is skipped with needs --force: orphaned: …, in a dry run too. In Trash mode it can be moved to the Trash instead, which you can undo;
  • is checked again right before deletion: same .git file, git data still missing, and no other repository or worktree inside. An orphan that contains another repository is only reported: rm -rf would take that repository with it.

The item's note suggests how to recover the code: clone the remote again, copy the changes into a new worktree when only its entry was pruned, or relink the folder with git worktree repair when you moved the main repository. In the picker, you must type yes to confirm a selection that contains caution items or items that need --force.

lu-cleaner reads each tool's state to warn you before you remove a worktree the tool still uses. It never modifies that state. This is what each tool sees after a removal:

Tool After the worktree is removed Better first step
Codex lu-cleaner doesn't touch the thread's history, but the thread still points to the missing folder as its working directory, so a resumed thread has no checkout to work in. Worktrees with active threads are caution. When the Codex app has already queued a worktree for archival, the note says so: the app snapshots and removes it itself. Archive the thread in Codex, which snapshots the worktree first
Cursor Agent changes that were not applied yet only existed in the worktree, so they are gone. Cursor creates new worktrees when it needs them. Apply or discard the agent's changes first
Conductor Conductor shows the workspace as missing. Worktrees of non-archived workspaces are caution. When a workspace is already archived in Conductor, the note says so and removal is uneventful. Archive the workspace in Conductor, which keeps its archive commit
Claude desktop The app keeps worktrees after a session is archived. Removing them is the cleanup it leaves to you. Worktrees of an active session are caution. Archive the session first
Claude Code (CLI) Committed work stays on the branch (for example worktree-onboarding). Ignored files (.env, node_modules) are gone. Commit what you want to keep
Created by hand The branch stays. Ignored files and native builds are gone. None

lu-cleaner worktrees --yes cleans without the picker, with the same rules as clean --yes:

Terminal window
# Preview: recommended worktrees only, nothing is removed
lu-cleaner worktrees --yes --smart --dry-run
# Remove them
lu-cleaner worktrees --yes --smart
# Codex worktrees idle for a month (preview)
lu-cleaner clean --yes -k codex-worktree --older-than 30d --dry-run

Caution worktrees (dirty, locked, orphaned, in use, with an active session, with secret files or a nested repository) are left out unless you pass --risk caution. So are worktrees with a warning, such as unpushed commits on a branch: the plan lists them under Held back. Even with --risk caution, dirty, locked, nested and detached-commit worktrees are still refused at clean time, and orphans are not deleted, unless you also pass --force.

A skipped item is not a failure: the command still exits with code 0. The summary lists each skipped item with its reason:

Message What to do
N uncommitted change(s) — commit them or use --force Commit, stash or discard the changes, or remove only the artifacts
detached HEAD with N commit(s) on no branch — they would be lost; … Run git branch <name> inside the worktree, then run the removal again
worktree is locked (git worktree lock) — use --force If the tool that locked it is done, run git worktree unlock <path>
contains another worktree, … or contains the git repository … Remove or move the nested checkout first
needs --force: orphaned: … Look inside the folder, then run again with --force if nothing is worth keeping
git records this worktree at …, not here: … Run the suggested git worktree repair (moved folder), or delete the copy yourself
in use: process 1234 is working inside … Quit the shell, dev server or agent running there
1 active Codex thread since the scan — rescan (or a Claude desktop session, a Conductor workspace) An agent started using it: archive the thread or session first
current directory is inside … Run lu-cleaner from another folder
… changed since the scan (different inode) — rescan Scan again: the folder was replaced
not possible in Trash mode (it would delete permanently) Run again without --trash (or with --trash=false when use_trash is set)
git refused to remove it (…) — use --force Read git's message; it has its own reasons