Workflow component regenerates its script to the latest plugin version but keeps a stale pluginRoot from earlier in a long session
Summary
In a long-running session, a custom Workflow component (provided by a plugin installed via a marketplace) regenerates its own backing script to the latest installed plugin version on every invocation — but the pluginRoot value derived from ${CLAUDE_PLUGIN_ROOT} that gets threaded into that same script's arguments stays pinned to whichever plugin version was active earlier in the session, even after several plugin updates have landed in the cache in the background. The result is a freshly regenerated script that expects a file only present in a newer plugin version, while being told to resolve paths against an old, no-longer-matching cache directory.
Environment
- Claude Code: 2.1.286
- Plugin installed via a marketplace (GitHub-backed, not a local-directory source)
- The plugin received roughly a dozen updates in
~/.claude/plugins/cache/<marketplace>/<plugin>/over about 24h while the session stayed open continuously (several/compacts in between, no restart)
Steps to reproduce
- Install a plugin that exposes a custom
Workflowcomponent whose generated script receives apluginRootargument derived from${CLAUDE_PLUGIN_ROOT}, and uses it to locate helper scripts under its own plugin root (e.g.${pluginRoot}/templates/some-helper.sh). - Open a long-running interactive session and invoke that
Workflowcomponent once, early on. - While the session stays open (no restart), let the plugin receive several updates in the background, so
~/.claude/plugins/cache/<marketplace>/<plugin>/accumulates newer version directories and~/.claude/plugins/installed_plugins.json(scope: "user") reflects the newest one. - Invoke the same
Workflowcomponent again later in the same session — ideally one whose newest version added a new helper file under the plugin root that an older version didn't have. - Inspect the freshly generated script on disk, and the
pluginRootvalue actually threaded into its arguments at runtime.
Expected
Either:
${CLAUDE_PLUGIN_ROOT}is re-resolved against the currently installed version each time theWorkflowcomponent is invoked, consistently with how the component's own backing script gets regenerated, or- it's made clear and discoverable that this value is captured once per session and will not track later updates, so a plugin author knows to defensively re-resolve it instead of trusting it.
Actual
- The generated script backing the
Workflowcomponent does track the newest installed plugin version — its file size and an internal version string changed between two invocations inside the same session, right when a plugin update had just landed in the cache. - The
pluginRootvalue passed into that same generated script's arguments stayed pinned to an older version directory — one that predates a helper file the newer script now expects to find under that root. - The mismatch surfaces as
bash: <stale-root>/templates/<file>: No such file or directory(exit 127) from deep inside the component's own logic, with nothing indicating the real cause is a stalepluginRootrather than a genuinely missing file. - This produced the identical exit-127 failure across three independent, parallel invocations of the same
Workflowcomponent (different arguments, different target files/branches) within the same session — consistent with one session-level stale value being the common cause, rather than anything specific to each invocation.
Related, but not duplicates
- #15642, #38699, #72616 — all closed, all about a stale
CLAUDE_PLUGIN_ROOT/ marketplace cache in some form, but each describes a different trigger: - #15642: a new session started after an update still resolving an old cached version.
- #38699: an inconsistency between hooks/skills (source dir) and the agent's Bash environment (cache dir); fixed in 2.1.94 for the startup-time case.
- #72616: the marketplace browser's displayed version not refreshing for a directory-source marketplace.
- None of these describe a value going stale after plugin updates that land while a session is already running, long past startup — which is the case here, and which is inconsistent with the fact that the same harness mechanism does regenerate the component's own script to the latest version on each call.
Workaround
Explicitly override the pluginRoot argument with the installPath read fresh from installed_plugins.json on each invocation, rather than relying on the harness-provided value.