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.
lu-cleaner worktrees # interactive picker (alias: lu-cleaner wt)lu-cleaner worktrees --list # the same list as a tablelu-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.
Where each tool puts its worktrees
Section titled “Where each tool puts its worktrees”| 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.
How worktrees are found
Section titled “How worktrees are found”lu-cleaner finds worktrees in four ways, so it also catches worktrees that are not in the usual folders:
- It walks the folders listed above, up to four levels deep, plus any folder in
worktree_rootsin your configuration. - It walks your project roots to find repositories. These are the
rootsin your config, or folders detected automatically such as~/dev,~/Projectsand~/conductor/repos. - It runs
git worktree listin every repository it found. This finds that repository's worktrees wherever they are. - 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
unknownuntil 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 exactgit -C … worktree remove …command to run yourself. - Your current directory. Run lu-cleaner from another folder.
Worktree statuses
Section titled “Worktree statuses”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
unknownand only reported.git worktree pruneis 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
cleanormerged. 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 saysupstream 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 |
Which worktrees are recommended
Section titled “Which worktrees are recommended”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
cleanormerged, 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_aftersetting 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 |
Remove only the artifacts
Section titled “Remove only the artifacts”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:
lu-cleaner artifactsThen 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.
What happens when a worktree is removed
Section titled “What happens when a worktree is removed”-
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 repaircommand to run.
If any check fails, the item is skipped with the reason.
-
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
.gitentry, or a bare repository), even in an ignored folder. Git would delete it along. Dependency folders such asnode_modulesandPodsare not searched. - HEAD is detached and some of its commits are on no branch, tag or remote-tracking ref.
git statusreports any change, tracked or untracked. lu-cleaner passes explicit flags, so astatus.showUntrackedFiles = nosetting 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-runruns these same checks and reports the same refusals. -
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 asnode_modules,Podsand.envdo 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/. -
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. -
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. -
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).
Stale worktree entries
Section titled “Stale worktree entries”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 prunewould 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.
Orphaned worktrees
Section titled “Orphaned worktrees”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 withneeds --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
.gitfile, git data still missing, and no other repository or worktree inside. An orphan that contains another repository is only reported:rm -rfwould 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.
What the tools see afterwards
Section titled “What the tools see afterwards”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 |
Clean worktrees from a script
Section titled “Clean worktrees from a script”lu-cleaner worktrees --yes cleans without the picker, with the same rules as clean --yes:
# Preview: recommended worktrees only, nothing is removedlu-cleaner worktrees --yes --smart --dry-run
# Remove themlu-cleaner worktrees --yes --smart
# Codex worktrees idle for a month (preview)lu-cleaner clean --yes -k codex-worktree --older-than 30d --dry-runCaution 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.
When a removal is skipped
Section titled “When a removal is skipped”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 |
Related
Section titled “Related”- React Native projects: the artifacts scanner in detail
- AI tools data: sessions, logs and caches of Codex, Claude and Cursor
- Safety model
lu-cleaner worktreesreference