[Bug] WorktreeRemove hook not invoked on subagent finish with WorktreeCreate hook
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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗