From 97e239a67c4ece342369c65a172152dff4d55c1f Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 3 Nov 2026 23:21:06 +0100 Subject: [PATCH] fix(install): stop listing plugin skills twice in Claude install.sh copies every skills-lock.json skill into .claude/skills while enable-plugins.js also enables the caveman and ponytail plugins that ship the same skills, so Claude listed 13 long descriptions twice on every turn of every session. enable-plugins.js now removes from the settings' .claude/skills every locked skill whose source is an enabled plugin's repository, taken from MARKETPLACES; .agents/skills keeps the copies for Codex and Gemini, which have no Claude plugin. The install message names it. test_dream checks that plugin copies go and other skills stay. Co-Authored-By: Claude Opus 5.5 --- scripts/enable-plugins.js | 10 +++++++++- tests/test_dream.py | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/enable-plugins.js b/scripts/enable-plugins.js index 1d5158c..bb9c9b9 100644 --- a/scripts/enable-plugins.js +++ b/scripts/enable-plugins.js @@ -49,6 +49,14 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) { } data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS); +const pluginRepos = new Set(Object.values(MARKETPLACES).map((entry) => entry.source.repo)); +const lock = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "skills-lock.json"), "utf8")); +for (const [name, entry] of Object.entries(lock.skills || {})) { + if (pluginRepos.has(entry.source)) { + fs.rmSync(path.join(path.dirname(settingsPath), "skills", name), { recursive: true, force: true }); + } +} + data.hooks = data.hooks || {}; const retired = (hook) => typeof hook.command === "string" && RETIRED.some((r) => hook.command.includes(`/skills/${r}`)); @@ -109,4 +117,4 @@ for (const [script, event, matcher, sub] of SCRIPT_HOOKS) { } fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n"); -console.log(`skills: enabled caveman + ponytail plugins, the ${REMINDERS.join(" + ")} reminders, the kvb-org-organize claim gate, the resource watch and the kvb-org-perform log in ${settingsPath}`); +console.log(`skills: enabled caveman + ponytail plugins (their skill copies dropped from this .claude), the ${REMINDERS.join(" + ")} reminders, the kvb-org-organize claim gate, news hook and resource watch, and the kvb-org-perform log in ${settingsPath}`); diff --git a/tests/test_dream.py b/tests/test_dream.py index bf92f3b..b6d2e16 100644 --- a/tests/test_dream.py +++ b/tests/test_dream.py @@ -161,6 +161,14 @@ class TestSettingsRegistration(unittest.TestCase): self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1) self.assertEqual(post.count("kvb-org-perform/scripts/log.py"), 1) + + def test_plugin_skills_are_not_copied_twice(self): + skills = Path(self.tmp.name) / "skills" + for name in ("caveman", "ponytail-review", "cavecrew", "archify", "kvb-dream"): + (skills / name).mkdir(parents=True) + self._patch() + self.assertEqual(sorted(p.name for p in skills.iterdir()), ["archify", "kvb-dream"]) + def test_existing_hooks_preserved(self): self.settings.write_text( json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "true"}]}]}}),