Plugin lifecycle gaps observed while building a statusLine plugin
Type: enhancement / platform feedback
Confirmed on: Claude Code v2.1.158 (macOS, Apple Silicon)
While building cc-cream — a status-line plugin distributed through a self-hosted marketplace — we ran a full install/update/uninstall QA pass and hit four behaviors that aren't bugs so much as gaps in the plugin lifecycle. None are documented, and re-discovering each took real effort. They'll affect anyone building a plugin that wires a status line or otherwise needs to touch settings.json. Filing them together since they tell one story; split however you like.
---
1. Plugins can't cleanly own the main statusLine
The plugin manifest only lets a plugin set agent / subagentStatusLine. The primary statusLine can't be declared in the manifest at all, so the only way to ship one is to have the plugin's own installer code write a statusLine block into the user's ~/.claude/settings.json. That creates three downstream problems:
${CLAUDE_PLUGIN_ROOT}doesn't expand in the statusLine execution context (it does in hooks). So the wired command can't resolve its own install path at render time, and therefore can't point at the currently-installed version. Our workaround is aSessionStarthook that re-pins the absolute path every session — purely to compensate for the missing variable expansion.- No uninstall path reverses the settings write.
/plugin uninstallremoves registry entries,enabledPlugins, and the plugin data dir, but leaves thestatusLineentry insettings.json(and leaves the entire cache tree on disk)./plugin marketplace removecascades further but still leaves both. Result: a status-line plugin keeps rendering after it's "uninstalled" — a zombie bar. The only reliable removal is a command the plugin itself ships, which is gone the moment the plugin is removed. - Combined, this means a status-line plugin can't be installed or removed cleanly through the host's own lifecycle commands.
Ask: let plugins declare a statusLine in the manifest (as they already can for agent/subagentStatusLine), expand ${CLAUDE_PLUGIN_ROOT} in that context, and have uninstall reverse what install wired.
In our code: src/install.js L33–68 builds the statusLine command and documents why ${CLAUDE_PLUGIN_ROOT} can't be used at render time (it isn't expanded there), so it bakes an absolute path behind a missing-file guard. hooks/auto-setup.js L1–42 is the SessionStart hook that exists solely to re-pin that path each session. src/install.js L85–105 is the plugin-shipped uninstall — the only reliable way to remove the wiring. src/cc-cream.js L118–185 is the renderer's own anti-zombie guard, which exits silently when it detects it's running orphaned from a cache dir after a partial uninstall.
2. claude plugin validate is more lenient than the install-time manifest schema
claude plugin validate . passes a plugin.json that declares commands as an array of file paths — but the real install path rejects exactly that with commands: Invalid input. (The working layout is a top-level commands/ dir with no commands key, auto-discovered.) Because validate is green, a broken manifest looks correct locally and then fails every real /plugin install. We shipped a non-working release this way before catching it with a live install test.
Ask: validation parity — claude plugin validate should reject anything the installer rejects, so it can be trusted as a pre-publish gate.
In our code: the working layout that install accepts — .claude-plugin/plugin.json with no commands key, and auto-discovered command files at the top level (commands/setup.md, commands/uninstall.md).
3. /plugin update never garbage-collects old version directories
Each installed version lands in its own dir under ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/, and /plugin update never removes the prior ones. They accumulate indefinitely.
Ask: prune superseded versions on update, or provide a claude plugin gc / prune command.
In our code: because the cache is never reclaimed, the renderer has to detect when it's executing from an orphaned cache dir and bow out — src/cc-cream.js L118–185.
4. No plugin-lifecycle hook event
SessionStart fires on startup and clear, but not on /plugin install or /reload-plugins. A plugin that needs to self-configure on first install therefore can't act at install time — it has to defer all wiring to the next session boundary, which is surprising to users who just installed it and see nothing happen.
Ask: a PluginInstall / PluginUpdate lifecycle event (or fire an existing hook on install) so self-configuring plugins can act immediately.
In our code: hooks/hooks.json registers on SessionStart because that's the only event available, and hooks/auto-setup.js L1–12 documents the resulting "wire on the first session after install" deferral.
---
Happy to provide repro steps, the QA probe notes, or the cc-cream source for any of these.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗