anthropics / anthropics/claude-code
[FEATURE] Fire WorktreeRemove (or a notification event) when Claude Code removes a git-backed worktree
- Lingua principale
- Python
- Stelle
- 145k
- Fork
- 23.1k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
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](https://github.com/anthropics/claude-code/issues/74708#issuecomment-5303442963)):
> `WorktreeRemove` is the cleanup counterpart to `WorktreeCreate`: it only runs for worktrees that a `WorktreeCreate` hook created (for example SVN or Perforce working copies), where it *replaces* `git worktree remove` and is expected to delete the directory itself. Worktrees Claude Code creates with `git worktree` are 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 ...` → `WorktreeCreate` fired, `SessionEnd` fired, `WorktreeRemove` did not.
- A subagent spawned with `isolation: "worktree"` → same tally: `WorktreeCreate` fired with `{"hook_event_name":"WorktreeCreate","name":"agent-ad7796546418a1f0f"}`, `WorktreeRemove` did 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](https://github.com/anthropics/claude-code/issues/74708#issuecomment-5303442963) 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:
- `SessionEnd` runs 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. `WorktreeRemove` output 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](https://github.com/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:
```json
{
"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 remove` and 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|false` field 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
- **`SessionEnd` hook.** 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 `WorktreeCreate` exists to avoid.
- **A wrapper that outlives the session.** Launching `claude --worktree` from 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
1. `.claude/settings.json` registers a `WorktreeCreate` hook. It runs `git worktree add`, clones the project's dev database into `picpac_`, writes `DATABASE_SUFFIX=` into the worktree's `.env.local`, and prints the worktree path.
2. `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.
3. Work finishes; `/exit`; the worktree is clean, so Claude Code removes it and deletes the branch.
4. **Today:** `picpac_issue-1234` is still there. It is still there after the next fifty worktrees too, and nothing in the session output ever said so.
5. **With this feature:** `WorktreeRemove` fires with `worktree_path` and `branch`, 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 remove` succeed 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 (`EnterWorktree` ignores both worktree hooks), #29716 (neither hook called in Claude Desktop).
- Version checked: 2.1.270 (macOS).
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Valutazione
Questa issue non è ancora stata valutata.