Deleting a session from claude agents skips plugin WorktreeRemove hooks ("hook failed" even when no hook ran)
Summary
My worktree hooks ship in a plugin (worktrunk-hook: a WorktreeCreate hook and a matching WorktreeRemove hook). With that plugin as the only source of worktree hooks, deleting a session from claude agents always fails with:
Worktree kept at <path> — could not be removed (WorktreeRemove hook failed); the session was not deleted
Two things are wrong:
- The delete path in
claude agentsdispatches settings-level WorktreeRemove hooks but never plugin-provided ones. The same plugin hook fires fine when a worktree is removed in-session (session exit, subagent finish) withCLAUDE_PLUGIN_ROOTresolved. Only the delete path strips it. - When that leaves zero hooks to run, the delete still reports "WorktreeRemove hook failed". No hook was executed and nothing is logged (the docs say WorktreeRemove failures only log in debug mode). That message sent me debugging the wrong thing for hours.
Evidence
I put a capture hook at settings level (logs stdin and env on every invocation) next to the enabled plugin hook wrapped with its own logger, then deleted sessions from claude agents:
- The settings-level hook fired every time. The payload is
{session_id, transcript_path, cwd, hook_event_name, worktree_path}and the caller is theclaude agentsprocess itself. - The plugin hook never fired and
CLAUDE_PLUGIN_ROOTwas absent from the dispatch environment. - The same plugin hook did fire on a session exit shortly after, with the root resolved to the plugin cache dir. The hook itself is fine.
- Strings in the 2.1.215 binary match this: the hook collection used by the delete path drops entries carrying a pluginRoot, and the dispatcher returns failure when the remaining set is empty.
Impact
A plugin is the natural place to ship worktree hooks as a pair. The create hook makes the worktree and the remove hook cleans it up. As shipped the pair is broken on exactly one path: sessions whose worktree came from the plugin can never be deleted from claude agents. The workaround is to duplicate the remove logic as a settings-level hook, which defeats shipping it in a plugin. Or resume each stuck session and clear its worktree state by hand before deleting.
Suggested fix
- Dispatch plugin WorktreeRemove hooks on the
claude agentsdelete path, withCLAUDE_PLUGIN_ROOTresolved, same as the in-session paths. - When zero WorktreeRemove hooks are registered for a dispatch, either fall back to the default removal or report "no WorktreeRemove hook available". "Hook failed" for a hook that never ran is what cost the debugging time.
Reproduction
- Install a plugin that registers WorktreeCreate + WorktreeRemove in its hooks.json (commands under
${CLAUDE_PLUGIN_ROOT}). No settings-level worktree hooks. - Start a background session so the job's state.json carries a worktreePath.
- Delete the session in
claude agents. - The delete fails with "WorktreeRemove hook failed". The plugin hook never ran (add logging to its command to confirm) and the session stays undeletable.
Prior issues
- #50398 (closed) looks like the same gap on the subagent-finish path. In-session removal does dispatch plugin hooks today; the
claude agentsdelete path doesn't. - #29716 is plugin worktree hooks not firing in Claude Desktop.
Environment
- Claude Code 2.1.215 / 2.1.216
- macOS (Darwin 25.5.0, arm64)
- Plugin: worktrunk-hook (pandysp/claude-plugins) at user scope
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗