[FEATURE] Fire WorktreeRemove (or a notification event) when Claude Code removes a git-backed worktree
Split out of #74708 at the maintainer's suggestion. That issue reported WorktreeRemove never firing at session exit; it was assessed as intended behavior with a docs problem (comment):
WorktreeRemoveis the cleanup counterpart toWorktreeCreate: it only runs for worktrees that aWorktreeCreatehook created (for example SVN or Perforce working copies), where it replacesgit worktree removeand is expected to delete the directory itself. Worktrees Claude Code creates withgit worktreeare removed by git directly and never run the hook [...] If you want a notification hook for git worktree removal, that would be a feature request rather than a bug in the current behavior.
This is that feature request. The docs have since been clarified ("For git-based worktrees, Claude Code handles cleanup automatically with git worktree remove"), so what remains is the missing capability, not the wording.
Problem Statement
There is no way to run teardown when Claude Code removes a git-backed worktree.
WorktreeCreate is an override hook — it replaces git worktree add — but it is also the only documented place to do per-worktree setup, and the docs actively point at side effects there:
If you need to copy local configuration files like .env into the new worktree, do it inside your hook script.
Setup that copies an .env is cheap to leak. Setup that provisions out-of-band resources is not. In my case the WorktreeCreate hook creates the git worktree and clones a per-worktree PostgreSQL database plus a DATABASE_SUFFIX env file for it. Every worktree session strands a database, because nothing fires on the way out.
Concretely, on 2.1.270 (macOS), in a fresh repo with WorktreeCreate, WorktreeRemove and SessionEnd hooks in the same .claude/settings.json, each appending its stdin to one log:
claude --worktree fr-probe -p ...→WorktreeCreatefired,SessionEndfired,WorktreeRemovedid not.- A subagent spawned with
isolation: "worktree"→ same tally:WorktreeCreatefired with{"hook_event_name":"WorktreeCreate","name":"agent-ad7796546418a1f0f"},WorktreeRemovedid not.
So the control is built in: the two worktree hooks sit in one file, one of them dispatches for a git worktree and the other never does. On the removal path itself, the maintainer's own repro on 2.1.233 shows the worktree and its branch being removed cleanly with no WorktreeRemove entry in the log.
Two things make this hard to work around rather than merely missing:
SessionEndruns while the session is still live and knows nothing about the worktree's fate — the exit prompt has not been answered yet, and on the subagent-finish path there is no session end at all.- The failure is silent.
WorktreeRemoveoutput is debug-only and cannot block removal, so a cleanup script wired to it looks correct, passes review, and does nothing — worse than a hook that fails loudly.
The asymmetry is the core of it: the documented setup hook fires for git worktrees, the documented teardown hook does not. Third-party tooling has been built on the assumption that they pair (for example rohansx/workz), and it silently no-ops today.
Proposed Solution
Dispatch a hook event when Claude Code removes a git-backed worktree, at each point where it removes one today: the --worktree exit prompt answered with "remove", a subagent with isolation: "worktree" finishing, ExitWorktree, and a background-session delete.
Preferred shape — fire the existing WorktreeRemove event for git worktrees too, with the payload it already documents:
{
"session_id": "abc123",
"transcript_path": "/Users/.../*.jsonl",
"cwd": "/Users/...",
"hook_event_name": "WorktreeRemove",
"worktree_path": "/Users/.../my-project/.claude/worktrees/feature-auth",
"branch": "worktree-feature-auth"
}
with two differences from the non-git contract, both necessary to keep the current git path intact:
- Notification, not override. Claude Code still runs
git worktree removeand still deletes the branch. The hook is told what happened; it does not become responsible for deleting the directory. A non-zero exit is logged, not treated as "removal failed" — otherwise adding the hook would change whether the worktree is removed at all. - Ordering. Either before removal (so the hook can read files out of the worktree) or after, as long as it is documented which. Before-removal is more useful and matches the non-git contract; a
removed: true|falsefield would let one script serve both.
Adding branch to the payload matters for the git case specifically: per-worktree resources are usually keyed by branch name, and worktree_path alone can't recover it once the worktree is gone.
If reusing the event is undesirable because the two contracts differ (override vs. notification), a separate event — WorktreeRemoved, say — with the same payload would serve just as well. What I need is an event; which name it has is secondary.
Alternative Solutions
SessionEndhook. Fires too early and in the wrong place: it runs from inside the live session, before the keep/remove choice is resolved, and it does not know whether the worktree survived. For the subagent-isolation: "worktree"path there is no session end to hook at all.- Polling
git worktree list. What we do now, and what at least one other reporter in #74708 fell back to. It works, but it is a reconciler for an event that is documented to exist, it needs a schedule and a place to run, and it cannot distinguish "removed" from "user is mid-rebase". - Not using the hook for provisioning. Means giving up per-worktree databases and doing setup by hand in every worktree, which is exactly what
WorktreeCreateexists to avoid. - A wrapper that outlives the session. Launching
claude --worktreefrom a shell function that cleans up afterwards works for the interactive path, but nothing covers subagent worktrees, which are created and destroyed inside the session.
Use Case Example
.claude/settings.jsonregisters aWorktreeCreatehook. It runsgit worktree add, clones the project's dev database intopicpac_<name>, writesDATABASE_SUFFIX=<name>into the worktree's.env.local, and prints the worktree path.claude --worktree issue-1234— the session gets an isolated checkout and an isolated database, so migrations and destructive specs in one worktree can't touch another.- Work finishes;
/exit; the worktree is clean, so Claude Code removes it and deletes the branch. - Today:
picpac_issue-1234is still there. It is still there after the next fifty worktrees too, and nothing in the session output ever said so. - With this feature:
WorktreeRemovefires withworktree_pathandbranch, the hook drops the database, and the invariant "a worktree owns a database for exactly as long as it exists" actually holds.
The same applies, more sharply, to subagents with isolation: "worktree": those are created and removed without any user interaction, so there is no manual moment at which cleanup could be attached.
Additional Context
- Split out of #74708 (closed as intended-behavior + docs fix).
- Related but distinct: #92342 asks for a hook that fires late enough — after the session process has released the worktree — for adopted worktrees Claude Code did not create, which on Windows is what makes
git worktree removesucceed at all. This request is narrower: worktrees Claude Code creates and removes itself, where removal already succeeds and the only thing missing is that anyone is told. - Related: #36205 (
EnterWorktreeignores both worktree hooks), #29716 (neither hook called in Claude Desktop). - Version checked: 2.1.270 (macOS).