Feature request: a hook that fires after a session releases its worktree, so cleanup can succeed
What I am asking for
A hook event that runs at a moment when removing the session's own git worktree can
actually succeed. That means two things at once:
- The session process has already released the worktree directory.
- The working directory is outside the worktree.
No hook today can meet both. A hook is spawned by the session process, so that process
is alive while the hook runs, and its current directory is the worktree. On Windows that
is enough to make removal fail.
Why the existing hooks do not cover this
SessionEnd runs from inside the live session. The session still holds the worktree.
WorktreeRemove exists and does the right job, but only for worktrees Claude Code
manages itself: ones created through EnterWorktree or agent worktree isolation, removed
through ExitWorktree or the interactive keep-or-remove prompt on exit. A session
launched into a worktree that Claude Code did not create never reaches that path, so the
hook never fires. That covers git worktree add by hand and worktrees made by a project
script, which is how my team creates every one of them.
Measurement
Windows 11, git 2.55.0.windows.5, Claude Code 2.1.259. Two arms, differing only in
whether a live process holds the worktree as its current directory. Both arms run the
same command, from outside the worktree.
Arm A, with a process whose cwd is the worktree:
$ git worktree remove ../tree
error: failed to delete '.../wtB/tree': Permission denied
exit=255
Arm B, identical command, no such process:
$ git worktree remove ../tree2
exit=0
So the failure is caused by a live process holding the directory, not by the command or
the path. Changing directory first does not help, because the session process is the
holder.
The failure is not clean
Arm A did not leave the worktree intact. Despite exit 255, git had already deregistered
the worktree and deleted its contents:
$ git worktree list
.../wtB/main a8c4ce6 [master] # the worktree is gone from the list
$ ls -a .../wtB/tree
. .. # empty directory left behind
Trying it from inside the worktree is worse. The first command strips the .git link, so
the directory stops being a git repository and a retry cannot even find one:
$ git worktree remove .
error: failed to delete '.../wt': Permission denied
exit=255
$ git worktree remove .../wt
fatal: not a git repository (or any of the parent directories): .git
exit=128
The result is an orphaned directory plus a branch that is no longer attached to anything
the user can see. It accumulates. The repository I am working in has 20 linked worktrees
registered right now.
Proposal
In order of preference.
- Dispatch
WorktreeRemovefor adopted worktrees too, from a process that outlives
the session. Reuse the event and payload that already exist; widen when it fires. The
payload already carries worktree_path, which is all a cleanup script needs.
- Add a post-exit event, fired by whatever supervises the session, once the session
process is gone. It would carry worktree_path, the branch, and a cwd outside the
worktree.
- At minimum, make the existing
cleanupWorktreepath apply to adopted worktrees, so
the WorktreeRemove hook at least gets offered the chance. Claude Code already knows
how to do the hard parts here: it changes directory out of the worktree first, it
refuses when the tree is dirty, and it skips cleanup when another live session is using
the same worktree.
Any of the three would work for me. What I cannot do is write a hook that runs late
enough.
What happens today
Cleanup is manual. The project I work in ships a PowerShell script that removes a
worktree, and its own documentation states the constraint plainly: git cannot remove the
worktree you are standing in, so the script has to be run by hand, from a different
checkout, after the session is gone. Nothing fires automatically, and no hook can call
that script, because the session that would call it is the process holding the directory.
The one workaround I can see is a SessionEnd hook that spawns a detached helper, which
polls until the session process exits and only then removes the worktree. I have not built
it, because it is a poor trade: the helper is an orphan process with no supervision,
nothing reports when it fails, and a restart in the wrong moment leaves the worktree
behind anyway. A hook that fires at the right time removes the need for any of it.
Related issues
- #26725, stale worktrees are never cleaned up. Same underlying pain, asks for automatic
cleanup and a claude --cleanup command rather than a hook.
- #41740, worktree removal fails on Windows because processes hold file locks. Closed as
not planned. Same mechanism as Arm A above, seen from inside Claude Code's own cleanup.
- #37611,
WorktreeCreatehook disables the worktree cleanup prompt on exit. - #64509, option to auto-remove worktrees on session exit.
Environment
- Claude Code 2.1.259, desktop app
- Windows 11 Pro 26200
- git 2.55.0.windows.5
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
There could also be an automatic cleanup preference associated with this worktree checkbox:
<img width="338" height="92" alt="Image" src="https://github.com/user-attachments/assets/9a3bbe08-5314-41f1-b072-b05f4f91d2fc" />
Still open. The ask is one hook event that fires at a moment when removing the session's own git worktree can succeed, which needs two conditions at once: the session process has released the directory, and the working directory is outside it. No hook today can meet both, because a hook is spawned by the session process, so that process is alive while the hook runs and its current directory is the worktree.
SessionEndruns from inside the live session, so the worktree is still held.WorktreeRemovedoes exactly the right job but only for worktrees Claude Code manages itself, created throughEnterWorktreeor agent worktree isolation and removed throughExitWorktreeor the keep-or-remove prompt. A session launched into a worktree Claude Code did not create never reaches that path, so the hook never fires. That coversgit worktree addby hand and worktrees made by a project script, which is how every one of ours is created.The measurement in the report is a two-arm comparison differing only in whether a live process holds the worktree as its current directory, with the same command run from outside the worktree in both arms. On Windows the arm with the live process fails with
Permission deniedand exit 255.Adding a data point from ~9 weeks of tracking this on our end (Windows 11, several repos, desktop app versions across the 2.1.2xx line through current 2.1.273).
We've logged 10 confirmed occurrences of what looks like the same underlying
WorktreePooldefect, in a few different shapes:git worktree addon reuse. The recreated folder has no.gitfile, so any git command run inside it silently operates on the shared repo root instead — and the new session's branch gets checked out there.git checkout -- .+git clean -fdon a worktree it knew was dirty, before recycling it. 4 files of uncommitted work destroyed — confirmed unrecoverable (mtimes matched the wipe event, emptygit diff, nothing in stash/reflog).git reflogshowing an unexpectedcheckout: moving from <sha> to <other-branch>entry mid-run, invalidating a 24-minute test run in one case.git worktree listshows a properly registered worktree, real.gitfile — but the desktop app pointed a new/resumed session at the wrong one, landing it on someone else's branch and uncommitted work. Confirmed with a fully correctcwdargument reaching the spawn call, ruling out a client-side path bug — this is squarely inside the closed-sourceWorktreePoolslot-assignment logic. One instance also silently overwrote the session's own display title with the misrouted task's title.git worktree listbetween two tool calls,.gitpointer gone, working directory emptied — no data lost that time only because the tree happened to be clean already.No fix or maintainer acknowledgment found in the changelog through 2.1.273 (checked back to early September). We're not asking for anything new here beyond flagging that this is still live, still hitting multiple people, and spans more than one repo/project on the same machine, so it isn't scoped to one project's worktree count or usage pattern.
Happy to share full incident detail (logs,
git reflogoutput, timestamps) if useful for reproduction — we've been keeping a structured log locally.