Plugin manager: expose a `post-update` migration hook for breaking plugin redesigns
Plugin manager: expose a post-update migration hook for breaking plugin redesigns
Summary
When a plugin ships a major-version update that fundamentally changes the agent surface (e.g. 80 specialist agents → 1 orchestrator + 17 commands, or vice versa), the plugin manager has no way for the plugin author to declare cleanup actions that should run once on the upgrade boundary. The result is that the new version installs alongside the old, both directories survive on disk (one orphan-flagged but not garbage-collected — see companion issue #1), and external tooling that assumes the old layout can silently keep the old version "hot" by walking the cache directory.
Concrete case
accessibility-agents v3.2.0 → v4.0.0 (May 2026):
- v3.2.0: 80 specialist agents registered, each consuming ~150–250 tokens of agent-list schema (≈12–18k tokens total).
- v4.0.0: 1 orchestrator agent (
accessibility-lead) with inline checklists, plus 17 three-line slash commands. ≈ <1k tokens of agent-list schema.
The upgrade went through the marketplace channel correctly. But:
- v4.0.0 was orphan-flagged by the plugin manager (incorrectly — the user had it enabled in
enabledPlugins). - v3.2.0 was retained in cache with live
.in_use/<pid>locks. - An installer-side shell script (
~/.claude/.a11y-agent-team-update.sh) installed during v3.x ran nightly via launchd and re-populated agent files into the plugin cache, picking v3.2.0 alphabetically. - The combined effect was that a session loading the plugin would see the v3.2.0 agent registry — the 80-specialist surface — even though
plugin.jsondeclared v4.0.0 with 1 agent. - The user saw a "cumulative agent descriptions ~15.5k > 15.0k tokens" warning every session and was unable to resolve it without manually patching the updater script, manually orphan-flag-clearing v4.0.0, and manually deleting the legacy launchd plist.
A plugin-author-supplied post-update hook running on the upgrade boundary could have:
- Removed the legacy launchd plist (
~/Library/LaunchAgents/com.community-access.a11y-agent-team-update.plist). - Removed the shadow updater script (
~/.claude/.a11y-agent-team-update.sh) and its git clone (~/.claude/.a11y-agent-team-repo/). - Triggered an immediate
rm -rfof the old version directory rather than relying on orphan-GC TTL. - Printed a one-time migration notice to the user.
Proposed design
Plugin manifest gains a hooks field:
{
"name": "accessibility-agents",
"version": "4.0.0",
"hooks": {
"post-update": "scripts/post-update.sh",
"post-uninstall": "scripts/post-uninstall.sh"
}
}
post-updateruns after the new version is installed and the old version is orphan-flagged. Receives the prior version as$1.post-uninstallruns after the user disables the plugin or removes it entirely.- Plugin manager waits up to N seconds for the hook to exit, logs output, and surfaces a final status message to the user.
- Hooks run in the user's shell with the plugin's cache directory as CWD.
Sandboxing: hook scripts run with the same permissions as the Claude Code process. Plugins are already trusted code (they install agents and MCP servers); a post-update hook is no greater trust delegation than the existing model. Surface the hook intent in the install/update confirmation prompt.
Acceptance
- A plugin with
hooks.post-updatedeclared in its manifest has the script invoked when the plugin manager upgrades it from one version to another. - The hook receives the prior version number as
$1and can act on it (rm,mv,launchctl bootout, etc.). - Hook stdout/stderr is captured and surfaced in the plugin manager's update output.
- Hook failure (non-zero exit) is non-blocking by default but clearly reported. Optional manifest field
hooks.failureMode: "block"can opt into hard-fail.
See also
- Companion issue:
.orphaned_atflag has no garbage collector — #58880 (would have caught the v3.2.0 directory after 14 days even without a hook). - Companion issue:
.in_use/<pid>lock files leak stale PIDs — #58881 (contributed to v3.2.0 staying "live" past its real usage).
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗