[BUG] Workflow-tool agent worktrees are never cleaned up — neither WorktreeRemove hook nor default removal fires on teardown
Environment
- Claude Code: 2.1.165
- Platform: Windows 11 Home (10.0.26200)
- git: 2.45.1.windows.1
- Repo: ordinary local git repository (not bare, single remote)
- Hooks: user-level
WorktreeCreateANDWorktreeRemovecommand hooks configured in~/.claude/settings.json(both verified working when invoked directly — see "Hook sanity checks" below)
Summary
When a Workflow-tool script spawns agents with opts.isolation: 'worktree', the worktrees are created correctly (via the configured WorktreeCreate hook), but on agent/workflow completion no teardown of any kind happens:
- The configured
WorktreeRemovehook is never invoked (no process spawn, no log entry, no diagnostic). - The documented default behavior — "the worktree is auto-removed if unchanged" — does not happen either.
- Both the worktree directories under
.claude/worktrees/AND their auto-created branches (namedwf_<runId>-<n>) are left behind, even for agents that made zero changes (read-only probes).
Confirmed across a full CLI restart (so it is not a hook-config-loaded-at-startup staleness issue), and reproduced identically in three separate workflow runs (before the WorktreeRemove hook existed, after registering it mid-session, and in a fresh session after restart).
Reproduction
- Configure a
WorktreeCreatehook (ours serializesgit worktree addunder a lock per #34645) and aWorktreeRemovehook in~/.claude/settings.json. - From a session in a git repo, run a Workflow whose script spawns read-only agents with worktree isolation:
export const meta = { name: 'probe', description: 'worktree teardown probe', phases: [{ title: 'Probe' }] }
phase('Probe')
const probes = await parallel([1, 2].map(i => () =>
agent(`Read-only probe #${i}: report cwd and git rev-parse --short HEAD. Do not modify anything.`,
{ label: `probe-${i}`, model: 'haiku', schema: { type: 'object', properties: { cwd: { type: 'string' }, head: { type: 'string' } }, required: ['cwd', 'head'] }, isolation: 'worktree' })
))
return { probes }
- Wait for the workflow to complete (agents return successfully; their reported cwds confirm they ran inside
.claude/worktrees/wf_<runId>-1/-2). - Run
git worktree listandgit branch --list 'wf_*'.
Expected
Per the Workflow tool's own description ("isolation: 'worktree' gives the agent its own git worktree (auto-cleaned if unchanged)"): unchanged worktrees removed on teardown — via the configured WorktreeRemove hook since one is registered, or default removal otherwise.
Actual
$ git worktree list
G:/Code/<repo> <sha> [master]
G:/Code/<repo>/.claude/worktrees/wf_373862c2-4e2-1 <sha> [wf_373862c2-4e2-1]
G:/Code/<repo>/.claude/worktrees/wf_373862c2-4e2-2 <sha> [wf_373862c2-4e2-2]
$ git branch --list 'wf_*'
+ wf_373862c2-4e2-1
+ wf_373862c2-4e2-2
Both worktrees and both branches persist indefinitely. The WorktreeRemove hook produced no output/log at any point (it logs on every invocation, including failures).
Hook sanity checks (the hooks themselves are fine)
- WorktreeCreate hook: works — all isolated agents got correct worktrees branched from HEAD.
- WorktreeRemove hook invoked manually with
echo '{"cwd":"<repo>","name":"<wt-name>"}' | node <hook>.cjs: removes the worktree and its merged branch, exit 0. So the hook is functional; it is simply never called by the Workflow teardown path.
Why this matters more than disk clutter
#51596 reports that leftover agent branches are silently reused on name-prefix collision, contaminating future subagents with stale state. Combined with this bug, every worktree-isolated workflow run permanently accumulates wf_* branches that become a contamination vector for later runs, not just orphaned directories.
Related issues (same family, different entry points)
- #37611 — WorktreeCreate hook configured ⇒ session cleanup flow skipped, WorktreeRemove never called (
claude -wflow). This issue may share a root cause, but the Workflow-agent path additionally skips even the default unchanged-worktree removal. - #36205 — EnterWorktree tool ignores WorktreeCreate/WorktreeRemove hooks
- #39281 —
--worktree --tmuxskips both hooks - #43730 — worktrees accumulate in
.claude/worktrees/with no lifecycle management - #34645 — the concurrent-creation
.git/config.lockrace (why our WorktreeCreate hook exists)
Workaround
Manual orchestrator cleanup after every worktree-isolated workflow run:git worktree list → git worktree remove --force <path> and git branch -d <name> per stray.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗