●CHROME — Compliance API session retrieval now covers Claude in Chrome transcripts as well. Enterprise beta, added on September 18●2.1.278 — Claude Code has not moved past 2.1.278 since September 19. That release made the auto-mode classifier server-side by default, and the classifier itself is not billed●11/17 — The undocumented client-certificate fallback setting stops being accepted on November 17, fifty-six days out. An in-app warning starts on November 3●TMPDIR — Windows shells do not set TMPDIR. Commands written out of habit with $TMPDIR fail with Permission denied, and the failures are quietly retried●NEW — The morning a deprecation notice appeared: checking the version before rewriting the config●TOOLS — How far should approvals be automated? Lining up Claude Code hooks and permissions next to Antigravity's Deny / Ask / Allow makes your own boundary easier to draw●CHROME — Compliance API session retrieval now covers Claude in Chrome transcripts as well. Enterprise beta, added on September 18●2.1.278 — Claude Code has not moved past 2.1.278 since September 19. That release made the auto-mode classifier server-side by default, and the classifier itself is not billed●11/17 — The undocumented client-certificate fallback setting stops being accepted on November 17, fifty-six days out. An in-app warning starts on November 3●TMPDIR — Windows shells do not set TMPDIR. Commands written out of habit with $TMPDIR fail with Permission denied, and the failures are quietly retried●NEW — The morning a deprecation notice appeared: checking the version before rewriting the config●TOOLS — How far should approvals be automated? Lining up Claude Code hooks and permissions next to Antigravity's Deny / Ask / Allow makes your own boundary easier to draw
Taking Inventory of Worktrees That Were Supposed to Be Gone
Isolated worktrees my subagents created were never folded away. Here is what I measured in git 2.34.1 about when remove succeeds, the branches that outlive the trees, the 12 MB per-tree difference, and how I decide who owns cleanup.
I had a week where a client site fix and a small migration in one of my own apps were running side by side on the same machine. On Friday night I glanced at free space and the number was one order of magnitude smaller than I expected.
Looking for the cause, I ran git worktree list and found six entries I did not recognize. None of them carried a name I had chosen. They had been created when I let subagents isolate their work, and they had simply stayed.
I stopped there. Isolation is described as something that cleans itself up when nothing changed, so I had placed cleanup outside my own work entirely.
Cleanup belongs to whoever is left holding the leftovers, not to whoever created them. Since that night, before I decide whether to delegate isolation, I decide who counts the leftovers and how.
Trees That Should Fold Themselves Sometimes Don't
When a subagent isolates its work, a temporary worktree is created and — the description goes — removed afterward if nothing changed. For short research tasks, that is exactly what happens.
Run the same setup long enough, though, and some of them stay. A matching report sits in anthropics/claude-code issue #95644, where six accumulated in a single session and git worktree remove --force turned out to be required even though all of them were clean.
Most of the Japanese-language write-ups on this feature still assume the opposite — that unchanged trees disappear, so no cleanup is needed. What I saw on my own machine looked much closer to the issue than to the write-ups.
As an indie developer running unattended jobs, the part that worried me was not the leftovers themselves. It was that free disk space had become my only notification channel. So I started by measuring, locally, exactly when a tree cannot be folded away.
Where contains modified or untracked files, use --force to delete it Comes From
Cleanup ultimately depends on whether git worktree remove succeeds. I built a throwaway repository and ran the same command against different states. My git is 2.34.1.
# throwaway repositoryLAB="$HOME/wtlab"; mkdir -p "$LAB/main"; cd "$LAB/main"git init -q -b main .printf 'node_modules/\n.next/\ndist/\n' > .gitignoreecho "hello" > README.mdgit add -A && git commit -qm init# case 1: nothing left in the treegit worktree add -q "$LAB/wt-clean" -b feat-cleangit worktree remove "$LAB/wt-clean" # succeeds, exit 0# case 2: only gitignored build outputgit worktree add -q "$LAB/wt-ignored" -b feat-ignoredmkdir -p "$LAB/wt-ignored/node_modules/pkg"head -c 200000 /dev/urandom > "$LAB/wt-ignored/node_modules/pkg/blob.bin"git worktree remove "$LAB/wt-ignored" # succeeds, exit 0# case 3: one untracked scratch notegit worktree add -q "$LAB/wt-untracked" -b feat-untrackedecho "scratch" > "$LAB/wt-untracked/notes.txt"git worktree remove "$LAB/wt-untracked"# fatal: '.../wt-untracked' contains modified or untracked files, use --force to delete it# exit 128
Here is the whole matrix as I measured it.
State of the worktree
git worktree remove
Exit code
What cleanup needs
Empty of local files
Succeeds
0
Folds away as is
Only gitignored output (node_modules/ and friends)
Succeeds
0
Folds away as is
One untracked file
fatal: contains modified or untracked files
128
--force
A tracked file modified
Same as above
128
--force
Locked with git worktree lock
fatal: cannot remove a locked working tree
128
unlock or remove -f -f
Directory deleted by hand
Nothing to act on
—
prune (metadata only)
If you want the same table for your environment, check git --version first. This boundary lives in git, not in the agent, so it is worth running once on the version you actually have.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to list every leftover isolated worktree and orphaned branch in under ten minutes, using a read-only audit that deletes nothing
✦You will know that a single untracked scratch file is what stops automatic cleanup, so you can prevent the disk-full surprise instead of discovering it on a Friday night
✦You will be able to choose between worktrees, separate clones, and one serialized clone based on who owns cleanup rather than on the 12 MB each copy costs you
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The surprise was that the heavy artifacts are not what blocks cleanup. A full node_modules/ still lets remove succeed. A single line of text in a scratch file stops it.
The reason is in what the check reads. remove looks at what shows up in git status --porcelain: modified tracked files and untracked files. Anything matched by .gitignore stays out of that list by default.
cd "$LAB/wt-dirty"git status --porcelain # modified and untracked onlygit status --porcelain --ignored=matching # adds !! node_modules/
That is the part with real operational weight. A tree that automatic cleanup considers unchanged can still fail to be removed, simply because an agent left a note that .gitignore never mentioned. And most of those failures end up buried in logs, resurfacing later as a number on your disk.
Build output is removable; a scratch note is not. I keep that inverted-sounding sentence in front of me whenever I revisit a cleanup design.
I had always thought of putting build output in .gitignore as a size decision. Measuring it showed me it also decides whether cleanup can finish at all.
Two Things Outlive the Tree
I measured what remains after folding a tree away, and the leftovers come in two layers.
The first layer is branches. A branch created by git worktree add -b survives the removal of its tree. In my run, four removals left four orphaned branches behind.
The second layer is metadata. Delete the directory by hand and .git/worktrees/<name>/ stays, showing up in the listing as prunable.
All git worktree prune reclaims is that orphaned metadata. Locked trees survive it, and it never touches branches. So the feeling of "I ran prune, we're clean now" was not accurate in my case.
A Read-Only Inventory, Before Any Deletion
Before deciding what to delete, I put a tool in place that counts. It removes nothing and reports state, on-disk size, and orphaned branches.
#!/usr/bin/env bash# Inventory of isolated worktrees. Read-only: deletes nothing.# usage: worktree-audit.sh [repo path]set -ucd "${1:-.}" || exit 1git rev-parse --git-common-dir >/dev/null 2>&1 || { echo "not a git repository: $(pwd)"; exit 1; }report() { # $1=path $2=branch $3=flag (prunable/locked/empty) local p="$1" br="${2:--}" flag="$3" state note payload="-" if [ "$flag" = "prunable" ]; then state="prunable"; note="no working tree; prune reclaims metadata" elif [ "$flag" = "locked" ]; then state="locked"; note="needs unlock or remove -f -f" payload=$(du -sh --exclude=.git "$p" 2>/dev/null | cut -f1) else payload=$(du -sh --exclude=.git "$p" 2>/dev/null | cut -f1) local dirty; dirty=$(git -C "$p" status --porcelain 2>/dev/null | wc -l) if [ "$dirty" -gt 0 ]; then state="needs --force"; note="${dirty} modified or untracked" else state="removable"; note="remove will fold it away" fi fi printf '%-18s %-14s %-8s %-14s %s\n' "$(basename "$p")" "$state" "$payload" "$br" "$note"}MAIN="$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')"printf '%-18s %-14s %-8s %-14s %s\n' TREE STATE SIZE BRANCH NOTEP=""; BR=""; FLAG=""while IFS= read -r line || [ -n "$line" ]; do case "$line" in worktree\ *) P="${line#worktree }"; BR=""; FLAG="" ;; branch\ *) BR="${line##*/}" ;; prunable*) FLAG="prunable" ;; locked*) FLAG="locked" ;; "") [ -n "$P" ] && { [ "$P" = "$MAIN" ] || report "$P" "$BR" "$FLAG"; }; P="" ;; esacdone < <(git worktree list --porcelain; echo)echoecho "[branches whose tree is gone]"git for-each-ref --format='%(refname:short)' refs/heads | while read -r b; do git worktree list --porcelain | grep -q "^branch refs/heads/${b}$" || echo " orphaned: ${b}"done
Run against the throwaway repository, with all four states prepared, it prints this.
TREE STATE SIZE BRANCH NOTEwt-dirty needs --force 800K feat-dirty 1 modified or untrackedwt-gone prunable - feat-gone no working tree; prune reclaims metadatawt-locked locked 12K feat-locked needs unlock or remove -f -fwt-review removable 12K feat-review remove will fold it away[branches whose tree is gone] orphaned: feat-clean orphaned: feat-ignored orphaned: feat-manual orphaned: feat-untracked
The important detail is that the verdict comes from counting git status --porcelain. Reading the same source remove reads gives you the warning in advance: this tree will not fold away without --force.
Why read-only? Because leftover trees include things left on purpose. I once folded away a tree that still held a half-finished check, and spent the next morning redoing it. Counting is automated here; deleting stays a thing I look at first.
The Order I Reclaim In
Run git worktree prune -v to reclaim metadata with no working tree. This step is safe enough to run without thinking twice.
Fold away whatever the inventory called removable with git worktree remove <path>.
For needs --force, read git -C <path> status --porcelain before deciding. Generated files only means --force; anything half-written gets rescued first.
For locked, unlock only the ones whose lock you can explain. Leave the rest.
For orphaned branches, check git branch --no-merged main first, and delete with git branch -d only once that output is empty.
# step 3, rescuing before removingP="$HOME/.claude/worktrees/wt-xxxxxx"git -C "$P" status --porcelaintar czf "$HOME/rescue-$(basename "$P").tgz" -C "$P" \ $(git -C "$P" status --porcelain | awk '{print $2}')git worktree remove --force "$P"
Step 5 mattered most to me. Four orphaned branches are fine to discard when all four are already contained in main. The moment one shows up unmerged, I stop deleting for the day and write the name down instead.
Worktree, Separate Clone, or One Clone at a Time
With those measurements in hand, I sorted isolation into three shapes and measured the disk cost too. In my Lab repository (a shallow clone), the main copy is 47 MB: 13 MB of .git plus 35 MB of working files.
Adding one git worktree cost 35 MB of files and 308 KB of metadata.
Cloning the same thing again locally cost the full 47 MB.
That is 12 MB per copy, about 25%. While your parallelism stays in single digits, it is not the deciding factor. So I choose on who owns cleanup instead.
Shape
Fits when
Who owns cleanup
Watch out for
Isolation via worktree
Several short tasks sharing one history
Whoever created it, assuming automatic cleanup will not run
Untracked files stop remove. Branches and metadata linger in two layers
Isolation via a separate clone
Dependency swaps and builds you don't want near your history
Deleting the directory finishes it
Each copy duplicates 13 MB of .git, and staleness needs managing
One clone, serialized
Short writes you can run in sequence
Nobody; nothing accumulates
index.lock contention. You cannot raise parallelism
I look after several apps of my own, including a wallpaper app, along with the Lab sites, a couple of WordPress blogs, and client sites, all from the same machine. On days when many jobs run side by side, I measure free space before I add more isolation.
Three lines, currently. Unattended jobs that write get exactly one persistent clone, and build output goes away after every run. When I do delegate isolation, the inventory becomes part of the daily routine and the leftover count gets recorded. And I never add --force automatically, because the moment I do, the difference between left on purpose and left behind disappears.
I trust self-folding isolation only after I have counted what failed to fold. So that a shrinking disk never becomes a source of unease, this is the order I keep.
If you try one thing today, run git worktree list --porcelain once and count two numbers: how many prunable lines you have, and how many orphaned branches. If both are zero, delegating isolation is fine as it stands. If either is not, it is worth putting an inventory in place the same day.
Thank you for reading this far. I hope it saves someone else a Friday night spent staring at free space.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.