Feature request: a hook that fires after a session releases its worktree, so cleanup can succeed

Status Open
Reported on v2.1.259
Maintainer reply None cached
Activity 4 comments · opened Sep 5, 2026

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:

  1. The session process has already released the worktree directory.
  2. 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.

  1. Dispatch WorktreeRemove for 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.

  1. 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.

  1. At minimum, make the existing cleanupWorktree path 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, WorktreeCreate hook 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

View original on GitHub ↗

3 Comments

wshallwshall · 1 month ago

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" />

wshallwshall · 1 month ago

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.

SessionEnd runs from inside the live session, so the worktree is still held. WorktreeRemove does exactly the right job but only for worktrees Claude Code manages itself, created through EnterWorktree or agent worktree isolation and removed through ExitWorktree or 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 covers git worktree add by 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 denied and exit 255.

IT-RT · 28 days ago

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 WorktreePool defect, in a few different shapes:

  1. Orphaned slot, falls through to shared root (first seen 2026-07-16): pool releases a worktree slot on session close but never re-runs git worktree add on reuse. The recreated folder has no .git file, 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.
  2. Active data loss (2026-08-03): pool ran the equivalent of git checkout -- . + git clean -fd on a worktree it knew was dirty, before recycling it. 4 files of uncommitted work destroyed — confirmed unrecoverable (mtimes matched the wipe event, empty git diff, nothing in stash/reflog).
  3. Mid-task rebind, not just at open/close (2026-08-19, repeated 2026-08-31 on a background subagent): the physical worktree directory gets silently repointed to a different branch/commit while a session is still actively running inside it — confirmed via git reflog showing an unexpected checkout: moving from <sha> to <other-branch> entry mid-run, invalidating a 24-minute test run in one case.
  4. Registered-but-wrong-target (2026-08-26, 2026-09-04, 2026-09-16 — this last one in a second, unrelated repo): git worktree list shows a properly registered worktree, real .git file — 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 correct cwd argument reaching the spawn call, ruling out a client-side path bug — this is squarely inside the closed-source WorktreePool slot-assignment logic. One instance also silently overwrote the session's own display title with the misrouted task's title.
  5. Deregistration mid-session (2026-09-07): a worktree that checked out clean minutes earlier vanished entirely from git worktree list between two tool calls, .git pointer 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 reflog output, timestamps) if useful for reproduction — we've been keeping a structured log locally.

Showing cached comments. Read the full discussion on GitHub ↗