claude-code - 💡(How to fix) Fix Plugin lifecycle gaps observed while building a statusLine plugin

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

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.

Fix Action

Fix / Workaround

  • ${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 a SessionStart hook that re-pins the absolute path every session — purely to compensate for the missing variable expansion.
  • No uninstall path reverses the settings write. /plugin uninstall removes registry entries, enabledPlugins, and the plugin data dir, but leaves the statusLine entry in settings.json (and leaves the entire cache tree on disk). /plugin marketplace remove cascades 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.
RAW_BUFFERClick to expand / collapse

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 a SessionStart hook that re-pins the absolute path every session — purely to compensate for the missing variable expansion.
  • No uninstall path reverses the settings write. /plugin uninstall removes registry entries, enabledPlugins, and the plugin data dir, but leaves the statusLine entry in settings.json (and leaves the entire cache tree on disk). /plugin marketplace remove cascades 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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Plugin lifecycle gaps observed while building a statusLine plugin