[Bug] WorktreeRemove hook not invoked on subagent finish with WorktreeCreate hook

Resolved 💬 3 comments Opened Apr 18, 2026 by jodyhagins Closed May 25, 2026

Bug Description
The following report was generated by Opus 4.7 based on an investigation into why the WorktreeRemove hok was not firing.

Title: WorktreeRemove hook never fires on subagent finish when WorktreeCreate hook is configured Version: Claude Code 2.1.114 Summary ------- The Task-tool subagent-finish path never invokes the WorktreeRemove hook for worktrees created via a user-configured WorktreeCreate hook. This contradicts both the public documentation and the stated rationale for providing the hook pair (cleanup of non-git VCS worktrees). Expected behavior (per docs at https://code.claude.com/docs/en/hooks) -------------------------------------------------------------------- "The cleanup counterpart to WorktreeCreate. This hook fires when a worktree is being removed, either when you exit a --worktree session and choose to remove it, or when a subagent with isolation: 'worktree' finishes. [...] If you configured a WorktreeCreate hook for a non-git version control system, pair it with a WorktreeRemove hook to handle cleanup. Without one, the worktree directory is left on disk." The "pair it with" instruction is only coherent if WorktreeRemove fires on subagent finish for hook-created worktrees. Actual behavior --------------- When a subagent is spawned with isolation: "worktree" and a WorktreeCreate hook is configured: - WorktreeCreate fires and the hook successfully creates a worktree. - Subagent runs to completion (with or without changes). - Subagent finish returns the worktreePath to the parent. - WorktreeRemove hook is NEVER invoked. - The worktree and branch are left on disk regardless of dirty state. Reproduction ------------ 1. Configure WorktreeCreate + WorktreeRemove hooks in settings, both logging the event. (A faithful-default WorktreeCreate implementation is fine; the same bug triggers even with a minimal printf '/tmp/wt'; mkdir -p /tmp/wt style hook as long as Claude sees any WorktreeCreate hook registered.) 2. Spawn a subagent via Task with isolation: "worktree". Have it do nothing (no reads, no writes). 3. Observe log: WorktreeCreate fires, subagent completes, WorktreeRemove is not logged. Worktree path persists on disk. Root cause (from binary analysis of 2.1.114) -------------------------------------------- In the Task-tool subagent-finish closure KH, the hookBased short-circuit returns before any cleanup path is considered: KH = async () => { if (!TH) return {}; let { worktreePath:jH, worktreeBranch:YH, headCommit:wH, gitRoot:e, hookBased:s } = TH; if (TH=null, s) return h(Hook-based agent worktree kept at: ${jH}), { worktreePath: jH }; if (wH) { if (!await P88(jH, wH)) return await YMH(jH, YH, e, !1, "agent_tool"), ..., {}; } return h(Agent worktree has changes, keeping: ${jH}), { worktreePath: jH, worktreeBranch: YH }; }; When hookBased is true, the function returns the path and logs "Hook-based agent worktree kept at: <path>" without calling YMH/ZmH (the WorktreeRemove dispatcher). There is no try/finally, no alternative fallthrough, and the YMH call below hardcodes its hookBased argument to false, so even if it were reached it would not dispatch the hook. All WorktreeRemove dispatch sites were enumerated in the binary: - VIH (interactive /worktree-exit) -> fires correctly - nf8 (bridge session cleanup) -> fires correctly (hookBased plumbed) - YMH via stale sweep -> hookBased hardcoded false - YMH via KH (subagent finish) -> unreachable (short-circuit above) For non-git VCS the situation is worse: headCommit (wH) is undefined since git rev-parse fails, so even if the short-circuit weren't there, the if(wH) branch is also skipped. There is no non-git-specific dispatch path. Impact ------ - Non-git VCS users cannot clean up hook-created subagent worktrees via the documented mechanism. This is the exact use case the docs call out. - Even for git users, hook-created subagent worktrees accumulate forever. - Any tooling that depends on WorktreeRemove as a lifecycle signal is silently broken for the Task-tool path. Suggested fix ------------- In KH, replace the early return: if (TH=null, s) return h(Hook-based agent worktree kept at: ${jH}), { worktreePath: jH }; with a call into YMH passing hookBased=true (or equivalent), so that the …
Note: Content was truncated.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗