[BUG] plugin update / auto-update compare only the version string, so plugins that ship new content without bumping version never refresh

Status Open
Reported on v2.1.226
Maintainer reply None cached
Activity 3 comments · opened Sep 9, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

claude plugin update and marketplace auto-update decide whether an installed plugin is out of date by comparing the version string alone. They never look at the resolved source commit. So a plugin whose author publishes new content on main without bumping plugin.json version is never refreshed, even though claude plugin marketplace update has already fetched the new commits into the marketplace clone and the marketplace has autoUpdate: true.

Concrete case: langsmith-skills@langsmith-skills (marketplace langchain-ai/langsmith-skills). Its plugin.json has said "version": "0.1.0" since the manifest was added on 2026-03-10 and has never changed. Since then the repo has landed 5 commits touching config/skills/ (the last one, e8f4120 on 2026-08-17, rewrote the langsmith-evaluator skill for the current LangSmith CLI). On 2026-08-24 the installed plugin cache was still the content from the original install, four months behind the marketplace clone sitting next to it on disk. claude plugin update langsmith-skills@langsmith-skills reported "already at the latest version (0.1.0)". The marketplace entry had autoUpdate: true in known_marketplaces.json; auto-update never re-pulled it either.

The only way to get the new content is claude plugin uninstall followed by claude plugin install.

The same comparison also affects the official marketplace's own source: {url, sha} pinned entries (e.g. superpowers, firecrawl): when the pinned sha moves but the plugin's version does not, plugin update reports "already at the latest version". I could not confirm the installed content was stale in that case because gitCommitSha in installed_plugins.json is itself unreliable (#86194), so I report it as a consequence of the same logic rather than an independently verified repro.

What Should Happen?

Freshness should be decided against the resolved source, not only the version string. Either:

  1. For github/git sources: compare the installed commit against the marketplace clone's current HEAD for the plugin's path; for url sources with a sha pin: compare against the pin. Update when they differ, even if version is unchanged. Or,
  2. At minimum, add claude plugin update --force (re-fetch and reinstall regardless of version) so users are not left with uninstall + reinstall as the workaround.

Marketplaces with autoUpdate: true should apply the same rule, since that flag currently implies a guarantee it does not deliver for unversioned plugins.

Error Messages/Logs

$ claude plugin marketplace update
Updating 3 marketplace(s)...✔ Successfully updated 3 marketplace(s)

$ git -C ~/.claude/plugins/marketplaces/langsmith-skills rev-parse --short HEAD
e8f4120

$ claude plugin update langsmith-skills@langsmith-skills
Checking for updates for plugin "langsmith-skills@langsmith-skills" at user scope…
✔ langsmith-skills is already at the latest version (0.1.0).

$ diff -rq ~/.claude/plugins/cache/langsmith-skills/langsmith-skills/0.1.0/config/skills \
           ~/.claude/plugins/marketplaces/langsmith-skills/config/skills
(differences listed: skill content in the cache is months older than the clone)

$ claude plugin uninstall langsmith-skills@langsmith-skills --scope user && \
  claude plugin install   langsmith-skills@langsmith-skills --scope user
(diff is now empty)

Steps to Reproduce

  1. claude plugin marketplace add langchain-ai/langsmith-skills and claude plugin install langsmith-skills@langsmith-skills --scope user at some commit of that repo (or any marketplace whose plugin keeps a fixed version).
  2. Wait for (or create, with a local marketplace) new commits to the plugin's skill files with plugin.json version unchanged.
  3. claude plugin marketplace update. Confirm the marketplace clone HEAD advanced.
  4. claude plugin update <plugin>@<marketplace>. Observe "already at the latest version".
  5. diff -rq the installed cache dir against the marketplace clone. Observe the cache is still the old content.
  6. Uninstall + install. Observe the diff is now empty.

Claude Model

Not model related.

Is this a regression?

No, as far as I can tell. The docs describe version as the update authority; this report argues that rule leaves unversioned and sha-pinned plugins permanently stale.

Last Working Version

N/A

Claude Code Version

2.1.226

Platform

macOS

Operating System

macOS (Darwin 25.6.0)

Terminal/Shell

zsh, Claude desktop app (Code tab) and CLI

Additional Information

Related but distinct: #86194 (gitCommitSha metadata not updated after a successful update), #86700 (plugin install sees a newer version but does not upgrade), #79776 (versionless official plugins re-trigger the updated banner). None of them cover the case where no version bump ever happens and content silently stays stale.

View original on GitHub ↗

3 Comments

wormeyman · 1 month ago

Another instance with the same shape, on a first-party plugin: cloudflare@cloudflare
from the cloudflare/skills marketplace.

.claude-plugin/plugin.json has said "version": "1.0.0" since it was added and has
never been edited. The marketplace.json entry declares no version at all, so the
comparison has nothing to work with from either side.

Installed 2026-03-17 at d311303. Measured 2026-09-10: the cache was still serving
skills dated 2026-03-16, while the clone sitting next to it was current to b052c32
(2026-09-07). 87 merged PRs and 219 commits landed in between. autoUpdate: true was
set the whole time, and claude plugin marketplace update had been refreshing the clone
without ever touching the cache.

One consequence here goes past stale docs. The plugin's .mcp.json changed too: it now
declares a single https://mcp.cloudflare.com/mcp, but my session was still connecting
to five server names Cloudflare has since retired (cloudflare-api, -bindings,
-builds, -docs, -observability). A frozen version string can leave users pointed at
endpoints that no longer exist, not only reading old guidance.

Uninstall plus install was the only fix here as well. I filed cloudflare/skills#196 for
the version bump on their side, but as you say, the client-side comparison is the general
problem.

RubenARNAUD · 1 month ago

One more case, measured, where claude plugin update is worse than "never refreshes": it can print a successful update while serving stale files, when the new version string was already installed once on the machine.

Setup: Claude Code 2.1.236 on Windows 11, a throwaway local-directory marketplace with a relative source entry, version set only in plugin.json. For each case I changed a skill file and the version, ran claude plugin marketplace update <mkt> then claude plugin update <plugin>@<mkt>, and compared a hash of the installed tree with the published one.

| Case | plugin update output | Installed files |
|---|---|---|
| content changed, version unchanged (0.1.0) | is already at the latest version (0.1.0) | stale |
| 0.1.0 → 0.1.1 · 0.1.9 → 0.1.10 · 1.0 → 1.0.0 | updated from … to … | fresh |
| downgrade to a version never installed here (0.1.10 → 0.0.5) | updated from 0.1.10 to 0.0.5 | fresh |
| downgrade to a version installed earlier (0.1.1 → 0.1.0) | updated from 0.1.1 to 0.1.0 | stale: the files of the first 0.1.0 install |
| back to a version installed earlier (0.1.0 → 0.1.1, e.g. after a revert) | updated from 0.1.0 to 0.1.1 | stale: the earlier 0.1.1 files |
| plugin uninstall then plugin install, same version | Successfully installed plugin | fresh |

What this shows:

  • The freshness check is plain string equality on the version: no semver ordering, so any different string triggers an update, downgrades included, and 1.0 ≠ 1.0.0.
  • When the resolved version already has a directory under ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/, update reuses that directory as-is instead of copying the source again, and still reports updated. Superseded version directories are not deleted on update: they only get an .orphaned_at marker, and the bundled CLI appears to sweep them 14 days later, so a version string that comes back within that window is served from the old copy.
  • install stages a fresh copy of the source, which is why uninstall + install fixes both this case and the one in the original report.

From the bundled code, the update path only asks the cache step to overwrite when the resolved version is unknown; otherwise an existing non-empty version directory is returned untouched. The same logic is still there in 2.1.266, the CLI bundled with the desktop app.

In addition to --force: when the target version's cache directory already exists, update should re-copy it or check it against the source before reusing it, and it should not report updated when the files it serves have not changed.

deruelle · 26 days ago

One reading that may sharpen the mechanism section of this report: the stale install is
detectable from the CLI's own record, and the CLI does not look at the field that would detect it.

Two identity fields, one comparator. On 2.1.228, an installed_plugins.json entry carried
version: 0.0.0-dev (a constant from the plugin manifest) and a valid 40-character
gitCommitSha simultaneously. claude plugin update compared version only — ✔ <plugin> is
already at the latest version (0.0.0-dev)
, exit 0, nothing delivered — while the installed content
was months behind the marketplace clone on the same disk and the gitCommitSha beside the constant
named the commit the install had actually come from. The datum that would prove the install stale is
already in the record; it is just not the one the comparator reads.

Controlled reading of what the version key changes. Two arms, identical except for the plugin
manifest's version key; marketplace entry keyless in both; clean HOME per arm; same CLI.

| Arm | plugin.json version | gitCommitSha recorded | version recorded |
|---|---|---|---|
| with-version | "0.0.0-dev" | yes | 0.0.0-dev |
| keyless | absent | yes | a 12-character commit string |

So the key does not suppress gitCommitSha; it decides what lands in version. Keyless manifests
get a commit-derived string there, which is why they update, and versioned manifests get a constant,
which is why they do not. That is consistent with the langsmith-skills and cloudflare cases
reported above — as described in those reports, both carry a constant version while the clone
advances.

plugin list --json cannot detect it either. Its output is a projection of
installed_plugins.json: editing that file's version and installPath to sentinels changed the
plugin list --json output verbatim, and restoring the file reverted it. There is no independent
read of installed content to compare against, so a consumer cannot script around the comparator from
the CLI surface.

On the gitCommitSha-is-unreliable caveat in the report above: #86194 is scoped to url-source
marketplace entries and is now closed. The reading here is on a github source, where the field
tracked the delivered commit correctly in every arm measured — so it is usable as a freshness
signal for exactly the source class option 1 names, which is the narrower claim.

Smallest fix consistent with the record: when a gitCommitSha is present for a github/git
source, compare it against the resolved source commit and update on difference regardless of
version. That uses a field the CLI already writes, and it makes autoUpdate: true mean what it
says for unversioned plugins. The --force fallback proposed above would also close the workaround
gap.

Earlier evidence from the same measurements, posted before 76882 was closed:
https://github.com/anthropics/claude-code/issues/76882#issuecomment-5273479508