From e774268d63706e560ff8f25d56905581e552ea38 Mon Sep 17 00:00:00 2001 From: xarmian Date: Wed, 7 Oct 2026 03:33:30 -0400 Subject: [PATCH] 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 --- internal/mcp/claude_plugin_manifest_test.go | 73 +++++++++++++++++++++ internal/server/handlers_watch_events.go | 2 + plugin/.claude-plugin/plugin.json | 1 - 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 internal/mcp/claude_plugin_manifest_test.go diff --git a/internal/mcp/claude_plugin_manifest_test.go b/internal/mcp/claude_plugin_manifest_test.go new file mode 100644 index 000000000..de4823b8d --- /dev/null +++ b/internal/mcp/claude_plugin_manifest_test.go @@ -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") + } +} diff --git a/internal/server/handlers_watch_events.go b/internal/server/handlers_watch_events.go index 1d1ade645..8c5a9b84c 100644 --- a/internal/server/handlers_watch_events.go +++ b/internal/server/handlers_watch_events.go @@ -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 diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 6635c3533..90266f95b 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -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",