Key takeaways
- The count of worktrees a session believes it created is exactly the thing that turns out to be wrong. So do not ask the session. Read its transcript.
- Anything written after leaving a worktree lands in the main checkout, on no branch. That is the single most likely thing to be lost forever, and the audit skipped it entirely until it cost me nineteen documents.
- Summary counts are a headline, not a guard. Five husk directories holding sixteen to twenty real files each were once offered up as “pure litter removal with no risk”.
- Every item gets an owner: mine, live, abandoned or other. Only mine can block. “Not mine, so I do not care” is how a repository fills up with other people’s abandoned work.
- An orphaned worktree with unpushed work is report-only. It may be the only copy in existence, so name it and leave it completely alone.
- The script does the deleting, never the model, and it re-checks every guard itself before it touches anything.
A Claude Code skill is a markdown file that teaches Claude a repeatable procedure. The Safe to Close skill teaches it to answer “safe to close?” with an audit: work out which worktrees, husk directories and branches this session actually created by reading the session’s own transcript, check the pull request behind every branch, and return the evidence rather than a feeling.
I run several Claude Code sessions against one repository at the same time. Each one takes its own git worktree and its own branch, because a single shared checkout has one HEAD and sessions switching branches thrash each other constantly. I have watched HEAD jump across three branches in the space of a few seconds.
That pattern works. What it also does is accumulate residue. Worktrees whose session was closed weeks ago. Directories git no longer tracks. Local branches that exist nowhere else in the world because nobody ever pushed them. And C:\projects has no backup, so “exists nowhere else” is the literal truth.
So at the end of a session I would ask: safe to close? And Claude would tell me, warmly and confidently, from memory.
The day the answer was wrong
On 8 September a session finished its work, left its worktree, and then wrote two more files: a handback goal file and a plan update, into the repository’s docs folder. Perfectly reasonable place for them. Except that leaving the worktree had put it back in the main checkout, on no branch, with nothing tracking those files.
The audit at the time reported blocking: 0. Safe to close.
A git clean would have taken both. So I added the main checkout as the first section of the audit, above everything else, and gave it the power to block. On the very next run it found nineteen uncommitted plan and goal documents sitting in that folder from that day’s other sessions.
Nineteen files, from one day, that every session involved had already been told were not its problem.
git clean -fdand every one of them is goneThat is the real failure and it is not a bug in git. It is that “did you clean up?” is a question about memory, and memory is the wrong instrument. The count of worktrees a session believes it created is exactly the thing that turns out to be wrong, because subagents create worktrees the parent never names, and work moves between trees without announcing it.
So the script does not ask. It reads.
Ownership comes from evidence. The running session’s own transcript records every worktree it entered and every branch it created or pushed, so the audit reads that instead of trusting a hand-typed list. It catches worktrees you would never have remembered to name.
Every item then lands in one of four buckets, and the bucket is the whole point:
| owner | means | treatment |
|---|---|---|
| mine | proven by this session’s transcript | the only thing that can block |
| live | another open session is pinned to it right now | never touched, never counted |
| abandoned | pinned to a session with no open window, or pinned to nothing | residue. Offer to clear it |
| other | ownership unproven, because no session API is configured | reported, never offered |
“Not mine, so I do not care” was the old behaviour, and it is how the repository fills up. A worktree whose session closed three weeks ago is nobody’s live work. It is litter.
Ownership comes from evidence, not memory. The session's own transcript records every worktree it entered and every branch it pushed, which catches the ones its subagents made.
What it checks, and why each one is on the list
| checked | why it matters |
|---|---|
| the main checkout | anything written after leaving a worktree lands here, on no branch |
| live worktrees | uncommitted files and unpushed commits die with the directory |
| husk directories | a directory git no longer tracks, and the number one cause of a session that can never resume |
| local branches with no worktree | work that exists nowhere else if it was never pushed |
| the pull request for every branch | number, state, and whether it actually merged |
The default branch comes from gh repo view, never from a guess. One of my repositories has beta as its default while a stale origin/HEAD still says main. Guessing produced false “never merged” blockers on every single run until I stopped guessing.
Merged branches on the remote are deliberately out of scope. There are hundreds of them, deleting a branch on a shared remote is an outward-facing act, and they harm nothing sitting there. They get one line and a count.
Read the column, not the summary
Every row prints its own clean flag, and that flag is the only thing that decides whether the delete path will touch it.
If a row says NO, it may not be called safe to remove, however harmless the summary made it sound. The delete path's own re-check was the only thing that stopped it.
This is not a stylistic preference. On the same day as the nineteen files, five husk directories, each holding sixteen to twenty real files, were presented to me as “pure litter removal with no risk”. The summary line said residue. The per-item warning said “19 real entries, inspect first” and had been truncated to “19…” by a table column. Only the delete path’s own re-check stopped it.
The Note column is never truncated now, and the rule is blunt: if a row says NO, it may not be described as safe to remove, however harmless the summary made it sound.
The three answers
A. Nothing left, everything merged. State the evidence in one line and ask nothing.
Yes, safe to close. Two worktrees this session, both merged (#119, #120), both directories removed, both branches deleted. Nothing on disk.
B. Everything merged, but something is still on disk. Say “yes, safe to close” first, because it is true, nothing of yours is at risk. Then offer the leftovers as a separate question, split by owner, with exactly what would be deleted listed before asking.
C. Anything of yours is blocked. Do not say safe to close, not even partially. Print every blocking item with its own next action:
worktree-foo dirty (3 files) -> commit and push before anything else
worktree-bar pushed, NO PR -> open a PR
worktree-baz PR #121 OPEN -> merge it, or say you are leaving it open
If several are blocked, list all of them. The question was for the list, not the first problem.
Two worktrees this session, both merged (#119, #120), both directories removed, both branches deleted. Nothing on disk.
one line of evidence, no questions askedNothing of yours is at risk. There are still 4 worktrees and 6 husks left by closed sessions.
- worktree-foodirty (3 files)commit and push before anything else
- worktree-barpushed, NO PRopen a PR
- worktree-bazPR #121 OPENmerge it, or say you are leaving it open
Two cases that look like rules and are not
A husk bricking an open session is a repair, not a trespass. That session cannot resume, ever, while that empty directory exists. Offering to remove it is fixing someone’s bug. “Never touch another session’s stuff” is about their work, and an empty husk is not work. Only ever for a husk with no real files in it.
An orphaned worktree with unpushed work is report-only. That directory may be the only copy in existence. Name it, say it has unsaved work, and leave it completely alone. Do not commit it, do not push it to a rescue branch, do not delete it. Somebody gets to decide that case by case, and it is not the model.
The deleting is the script’s job, not the model’s
Deletion goes through one path and one path only:
& "$env:USERPROFILE\.claude\skills\safe-to-close\audit-worktrees.ps1" `
-RepoPath "C:\code\your-repo" -Delete -Yes -Items "worktree-foo","husk-bar"
It re-runs the entire audit first and re-checks every guard itself, so it does not trust the list it was handed. It refuses any item that is dirty, unpushed, unmerged, owned by an open session, or outside the worktree root. Then it re-audits and prints the result.
Never do this by hand. Not Remove-Item -Recurse, not rm -rf, not rmdir /s. That is precisely the command that once followed a node_modules junction back into the main tree and wiped the whole thing. The script’s delete walk never descends into a reparse point. A hand-typed one will.
Run it as a goal
The audit is a good candidate for a Stop hook, because “did you clean up” is exactly the question a model answers too generously. GOAL-PROMPT.md is ready to paste: it runs the audit, closes every blocker it can close, and does not let the turn end until the audit exits clean or the remaining items are proven to belong to someone else.
If you want the skill that writes prompts like that one, I published it too.
Why this matters more when you work alone
On a team, lost work gets noticed by someone else. A colleague pulls the branch and it is not there. Standup surfaces it. The person who wrote it asks where it went.
Working on my own, nobody notices. There is no second pair of eyes on the repository, and the nineteen files would have gone quietly, on a Tuesday, with every session involved having been told it was clear. That is the actual risk of running a full stack as one person: not that the work is worse, but that there is nobody downstream to catch what went missing.
So the check has to be a machine, and it has to refuse to be reassuring. Same reason its sibling refuses to call anything done without a screenshot. The operating model only holds if the tools are the ones being sceptical.
Free and public on GitHub, MIT licensed. It needs git, the GitHub CLI, and PowerShell. It reports by default and deletes nothing until you say yes to a list you have already read.
Get the skill on GitHubWork with me 1-on-1What does the Safe to Close skill do?
It audits the repository instead of trusting what the session remembers: the main checkout, every worktree, every husk directory, every local branch, and the pull request behind each one. Then it gives one of three answers with the evidence attached.
Why not just ask Claude if it finished?
Because subagents create worktrees the parent never names, and work written after leaving a worktree lands somewhere the session is not thinking about. Memory is the wrong instrument for this question.
What is a husk directory?
A directory under the worktrees folder that git no longer tracks. It is the number one cause of a session that can never resume, because the CLI refuses to resume a session pinned to a path that still exists but is no longer a worktree.
Does it delete things?
Only through -Delete -Yes -Items, only after you approve a list you were shown first, and only after the script re-runs the audit and re-checks every guard itself. Dirty, unpushed, unmerged, live or out-of-root items are refused.
Do I need to run several Claude sessions for this to be useful?
It is built for that case, but the main-checkout check earns its keep on a single session. Files written after leaving a worktree land there on no branch, and that is the most likely thing to lose permanently.
What is the optional session API?
One HTTP endpoint that returns which sessions are currently open, so the script can tell live work from abandoned residue. It is unset by default. Without it the script still audits everything and still blocks correctly on your own work, it just reports everything else as other and offers nothing for cleanup.
Does it need the GitHub CLI?
Yes, for pull request state. A missing gh blocks, because unknown is not the same as merged.
