ExitWorktree can't remove a worktree launched via --worktree (or entered via path:), so WorktreeRemove hook never fires and per-worktree resources leak

Status Open
Reported on v2.1.236
Maintainer reply None cached
Activity 0 comments · opened Sep 11, 2026

Environment

  • Claude Code: 2.1.236
  • OS: macOS (Darwin)
  • Project has .claude/settings.json with WorktreeCreate and WorktreeRemove hooks configured, used to provision/tear down per-worktree build caches (Xcode DerivedData) and a cloned iOS Simulator.

Description

WorktreeCreate/WorktreeRemove hooks exist so a project can keep expensive per-worktree side effects (build caches, simulators, secret symlinks, etc.) in sync with the worktree's lifecycle. WorktreeCreate fires correctly when a session is launched via claude --worktree <name> — confirmed directly: the resulting worktree used our custom hook's path/branch convention rather than Claude Code's built-in default (repo root, origin/HEAD), and our ios/.claude project config was active for the whole session, which is only possible if the hook's chosen directory was actually used as the session cwd.

However, there is no corresponding way to trigger WorktreeRemove for that same worktree from inside the session. The ExitWorktree tool only tracks and permits removal of a worktree that was created via an in-chat EnterWorktree(name:) tool call in the same session. It refuses to act on:

  1. The worktree a session was launched directly into via claude --worktree <name>, or
  2. A worktree entered mid-session via EnterWorktree(path: "<existing-worktree>").

In both cases, the only way to actually remove the worktree is to bypass Claude Code entirely (raw git worktree remove, or deleting the directory), which never triggers WorktreeRemove — so any resources that hook was responsible for tearing down (in our case, several GB of Xcode DerivedData plus a cloned iOS Simulator) are silently leaked with no prompt, warning, or automated recovery.

Repro steps (case 1 — CLI-launched)

  1. In a project with .claude/settings.json defining a WorktreeCreate hook and a WorktreeRemove hook (the latter doing something observable, e.g. writing a marker file or deleting a directory it created).
  2. From that project directory, run claude --worktree my-feature.
  3. Confirm WorktreeCreate ran (e.g. the marker/artifact it produces exists).
  4. Do some work, then ask Claude mid-chat to "clean up this worktree," or directly invoke the ExitWorktree tool with action: "remove".
  5. Observed: ExitWorktree refuses / reports there is no active EnterWorktree session to exit, even though the running session's cwd is inside the worktree the hook created.
  6. As the only available path, remove the worktree manually (git worktree remove <path> from elsewhere, or have Claude do it via Bash).
  7. Observed: the WorktreeRemove hook never runs. Whatever it was responsible for tearing down (build cache, simulator, etc.) is left on disk indefinitely.

Repro steps (case 2 — entered via path:)

  1. Start a session anywhere in the project.
  2. EnterWorktree(path: "<path to some other pre-existing worktree>").
  3. Later, ask Claude to clean up that worktree, or invoke ExitWorktree(action: "remove").
  4. Observed: refused, per the tool's own documented behavior ("ExitWorktree will not remove a worktree entered this way; use action: "keep""). Same downstream leak as above once removal is done manually.

Expected behavior

Since WorktreeCreate/WorktreeRemove are meant to keep a worktree's lifecycle and its side effects in sync, there should be a supported way to trigger WorktreeRemove for any worktree the current session is rooted in or was pointed at — not only ones created via an in-chat EnterWorktree(name:) call. At minimum, ExitWorktree(action: "remove") should work for the worktree a session was launched directly into via --worktree, since that's the most common way these hooks get exercised in the first place.

Actual behavior

ExitWorktree silently limits its removal capability to worktrees it created itself in the same session via EnterWorktree(name:). Any other worktree can only be removed by working around Claude Code entirely, which bypasses WorktreeRemove with no warning that hook-managed resources are about to be orphaned.

Impact

For any project using these hooks to manage non-trivial per-worktree resources, every --worktree-launched session leaks those resources on cleanup unless the user separately remembers to run a project-specific manual GC script (ours: worktree-gc.sh --prune). This defeats the purpose of WorktreeRemove for what is likely the most common way these hooks get invoked.

Additional context

There's a separate, likely-unrelated history of WorktreeCreate hooks causing claude --worktree to hang indefinitely before the hook even runs (#42752, #41614) — last reported reproducing around 2.1.126, both auto-closed by inactivity rather than a confirmed fix. That's a different failure mode (hang vs. silent-skip-on-removal) and isn't what we're reporting here; noting it only in case it's the same root cause resurfacing differently.

View original on GitHub ↗