fix(plugin): drop the pinned version so installs follow the commit (BUG-3463) (#1848)

A version in plugin.json pins every installed copy until the string
changes (code.claude.com/docs/en/plugins/loading, "Versions and
updates"). 0.3.3 stood from 2026-08-27 while 8 commits changed plugin/,
so marketplace installs kept the August skills. With no version, a
relative-path plugin in a git-hosted marketplace is versioned by its
commit, and `claude plugin update` picks up every change.

Measured in a scratch CLAUDE_CONFIG_DIR against a git marketplace with a
commit to plugin/ after install: pinned 0.3.3 answered "already at the
latest version (0.3.3)" and kept the old copy; unpinned updated
be0122112e5a -> d6312f1d8c6d with the change in the cache.

A guard test refuses a version in plugin.json or the marketplace entry.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
This commit is contained in:
xarmian
2026-10-07 03:33:30 -04:00
committed by GitHub
parent a7c559e431
commit e774268d63
3 changed files with 75 additions and 1 deletions
@@ -0,0 +1,73 @@
package mcp
import (
"encoding/json"
"os"
"testing"
)
// BUG-3463: the Claude Code plugin must not pin a version. A `version` in
// plugin.json (or in its marketplace entry) pins every installed copy until
// the string changes, however many commits touch plugin/; with none, a
// relative-path plugin in a git-hosted marketplace is versioned by its
// commit, so `/plugin update` picks up every change. 0.3.3 stood from
// 2026-08-27 while 8 commits changed plugin/ under it.
// https://code.claude.com/docs/en/plugins/loading#versions-and-updates
const (
claudePluginManifest = "../../plugin/.claude-plugin/plugin.json"
claudeMarketplace = "../../.claude-plugin/marketplace.json"
)
func pinnedVersion(t *testing.T, path string, entries func(map[string]any) []map[string]any) []string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
var doc map[string]any
if err := json.Unmarshal(b, &doc); err != nil {
t.Fatalf("%s: %v", path, err)
}
var pinned []string
for _, e := range entries(doc) {
if v, ok := e["version"]; ok {
pinned = append(pinned, path+": "+jsonString(v))
}
}
return pinned
}
func jsonString(v any) string {
b, _ := json.Marshal(v)
return string(b)
}
func manifestEntries(doc map[string]any) []map[string]any { return []map[string]any{doc} }
func marketplaceEntries(doc map[string]any) []map[string]any {
var out []map[string]any
plugins, _ := doc["plugins"].([]any)
for _, p := range plugins {
if m, ok := p.(map[string]any); ok {
out = append(out, m)
}
}
return out
}
func TestClaudePlugin_NoPinnedVersion(t *testing.T) {
pinned := pinnedVersion(t, claudePluginManifest, manifestEntries)
pinned = append(pinned, pinnedVersion(t, claudeMarketplace, marketplaceEntries)...)
for _, p := range pinned {
t.Errorf("plugin version pinned at %s: installed copies stop updating until it changes; remove it (BUG-3463)", p)
}
// Control: the marketplace walk must reach the pad entry, or an
// entry gaining a version would pass unseen.
b, _ := os.ReadFile(claudeMarketplace)
var doc map[string]any
_ = json.Unmarshal(b, &doc)
if n := len(marketplaceEntries(doc)); n == 0 {
t.Fatal("marketplace.json lists no plugins; the guard is reading the wrong file")
}
}
+2
View File
@@ -720,6 +720,8 @@ func watchNotificationVisible(watches map[string]string, vis watchAccessVisibili
// traffic until PLAN-2558 S6 (TASK-2591) reworded it and bumped the
// plugin version — installed plugins are version-pinned at install
// (live-proven day-33), so the bump is what made the fix reach users.
// The manifest no longer carries a version (BUG-3463), so an update
// now follows the commit that changed plugin/.
// Push (IDEA-2544 Phase 1) is semantically DIFFERENT from assignment,
// not just a same-shaped variant, and that difference is why this
-1
View File
@@ -1,6 +1,5 @@
{
"name": "pad",
"version": "0.3.3",
"description": "Talk to your project. Pad is local-first project management for developers and AI agents — this plugin gives Claude Code a conversational Pad skill, typed shortcuts (/pad:status, /pad:capture, /pad:onboard, /pad:connect, /pad:disconnect), and consent-gated live item-change notifications.",
"author": {
"name": "Perpetual Software LLC",