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 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-04 17:07:23 +01:00
1 parent 031fd3deb4
commit 97e239a67c
2 files changed
+17 -1

No files matched your search

+9 -1
View File
@@ -49,6 +49,14 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) {
} }
data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS); 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 || {}; data.hooks = data.hooks || {};
const retired = (hook) => const retired = (hook) =>
typeof hook.command === "string" && RETIRED.some((r) => hook.command.includes(`/skills/${r}`)); 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"); 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}`);
+8
View File
@@ -161,6 +161,14 @@ class TestSettingsRegistration(unittest.TestCase):
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1) self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
self.assertEqual(post.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): def test_existing_hooks_preserved(self):
self.settings.write_text( self.settings.write_text(
json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "true"}]}]}}), json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "true"}]}]}}),