Plugin marketplace refresh can install unmerged or stale content without any signal
Environment
- Claude Code 2.1.236, Windows 11, Git for Windows.
- A private plugin marketplace hosted in a GitHub repository, added with
claude plugin marketplace add <owner>/<repo>. It is cloned over HTTPS into~/.claude/plugins/marketplaces/<marketplace>. - Everything below was reproduced in an isolated config directory (
CLAUDE_CONFIG_DIRpointing to an empty folder), except where noted.
1. A failed refresh makes plugin update report "already at the latest version"
claude plugin update <plugin>@<marketplace> refreshes the marketplace clone by itself: after the command, the clone's reflog shows pull origin HEAD: Fast-forward. When that refresh fails, nothing is reported.
Steps
- Put the marketplace clone on a commit where the plugin is
0.5.2, and install the plugin at0.5.2. The upstream default branch has0.5.3. - Make the network unavailable to git:
http.proxy=http://127.0.0.1:9, viaGIT_CONFIG_COUNT/GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n. - Run
claude plugin update <plugin>@<marketplace>.
Output (exit code 0, no warning)
Checking for updates for plugin "<plugin>@<marketplace>" at user scope…
✔ <plugin> is already at the latest version (0.5.2).
Same sandbox, network restored, same command:
✔ Plugin "<plugin>" updated from 0.5.2 to 0.5.3 for scope user. Restart to apply changes.
Expected: when the refresh fails, plugin update should say so and exit non-zero — at the very least, it should not claim the plugin is at the latest version. On a machine where the remote is unreachable for good, every later update keeps the stale content while reporting success.
2. The refresh merges the remote HEAD into whatever branch is checked out in the clone
The refresh runs git pull origin HEAD in the marketplace clone. If the clone is not on the default branch — say a branch was created there — the remote HEAD is merged into that branch. A local merge commit is created, and later installs and updates take their content from that branch, including commits that were never pushed or merged upstream. Nothing is printed.
Observed on a real machine — the marketplace clone had been left on a feature branch. Right after claude plugin marketplace update <marketplace>, its reflog showed:
pull origin HEAD: Merge made by the 'ort' strategy.
Reproduced in the isolated config directory (network available):
- Check out a local branch
workin the marketplace clone, starting from an older commit (plugin at0.5.2). Add one local commit that is never pushed: one line appended to a skill file of the plugin. - Run a command that refreshes the marketplace. In this run it was
claude plugin install <plugin>@<marketplace>; the samegit pull origin HEADalso comes fromplugin updateandmarketplace update.
The only output:
Installing plugin "<plugin>@<marketplace>"...✔ Successfully installed plugin: <plugin>@<marketplace> (scope: user)
Clone reflog afterwards:
pull origin HEAD: Merge made by the 'ort' strategy.
commit: wip: local commit, never pushed
Result:
- The clone is still on
work, and its HEAD is a merge commit whose parents are the local commit and the upstream HEAD. - The plugin is installed at
0.5.3, and the installed skill file contains the line from the never-pushed commit.
Expected: the marketplace clone is a cache, and the CLI already treats it as one elsewhere. When the clone's origin pointed to an unreachable path, claude plugin marketplace update printed Found stale directory, cleaning up and re-cloning… and re-cloned it from the declared source. The normal refresh should be just as strict: align to the declared source's default branch (e.g. git fetch + git reset --hard origin/<default>), or refuse with an error when the clone is on another branch or has local commits. It should never merge into it.
Why it matters
Plugins carry skills and instructions that every session loads. Both behaviours change what gets loaded without any signal: stale content reported as up to date, or local commits that were never reviewed installed as if published.