From bf8880c5b93787f403ea36b7146dcc1b8a7f2a3d Mon Sep 17 00:00:00 2001 From: CodeWithJuber Date: Sat, 22 Aug 2026 19:58:03 +0000 Subject: [PATCH] fix: stop re-declaring the auto-loaded hooks file in the plugin manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code loads the standard hooks/hooks.json automatically; declaring it in manifest.hooks registered the same file twice and the loader rejected the entire plugin ('Duplicate hooks file detected: ./hooks/hooks.json'), taking all 18 skills, 5 crew agents, every ambient guard and the forge-cortex MCP server down with it. manifest.hooks is only for additional hook files. - .claude-plugin/plugin.json: drop the redundant hooks entry. - src/doctor.js: the plugin-hooks check now defaults to the standard path when manifest.hooks is absent, and warns when a manifest re-declares the auto-loaded file (the exact failure this fixes). - test/channels.test.js: regression test — fails if the standard hooks path is ever re-declared. Verified it catches the old manifest. - CHANGELOG.md: Fixed entry under Unreleased. Gate: 1129/1131 pass (2 skipped), biome clean, tsc clean, docs check exit 0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Pyb7qWVaCdDzkQ1CfiaBk2 --- .claude-plugin/plugin.json | 3 +-- CHANGELOG.md | 10 ++++++++++ src/doctor.js | 17 ++++++++++++++--- test/channels.test.js | 17 +++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 864698f..55634c2 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -16,6 +16,5 @@ "./global/crew/independent-reviewer.md", "./global/crew/scout.md", "./global/crew/verifier.md" - ], - "hooks": "./hooks/hooks.json" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index bea3e3d..47d7982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed + +- **Plugin load: dropped the duplicate `hooks` declaration from the manifest.** Claude Code + loads the standard `hooks/hooks.json` automatically, so `manifest.hooks` pointing at that + same path registered it twice and the loader rejected the entire plugin + (`Duplicate hooks file detected`) — taking every skill, agent, guard and the `forge-cortex` + MCP server down with it. `manifest.hooks` is for additional hook files only; the guards are + unchanged and still load from the plugin root. A regression test in `test/channels.test.js` + now fails if the standard path is ever re-declared. + ## [0.32.0] - 2026-08-14 ### Changed diff --git a/src/doctor.js b/src/doctor.js index 37e6321..80ed010 100644 --- a/src/doctor.js +++ b/src/doctor.js @@ -291,9 +291,20 @@ function commandScriptFromPluginRoot(hook) { function checkPluginCompatibility(out) { try { const plugin = readJson(join(BRAND.root, ".claude-plugin", "plugin.json")); - const hookRel = plugin.hooks; - const hookPath = hookRel ? join(BRAND.root, hookRel) : ""; - if (!hookRel || !existsSync(hookPath)) { + // Claude Code auto-loads the standard hooks/hooks.json from the plugin root; + // manifest.hooks exists only for ADDITIONAL hook files. Re-declaring the standard + // path registers it twice and the loader rejects the WHOLE plugin. + const standardRel = "hooks/hooks.json"; + const hookRel = plugin.hooks ?? standardRel; + const hookPath = join(BRAND.root, hookRel); + if (plugin.hooks && hookPath === join(BRAND.root, standardRel)) { + out.push( + warn( + "Claude plugin hooks", + "manifest re-declares the auto-loaded hooks/hooks.json — Claude Code rejects the whole plugin (duplicate hooks file)", + ), + ); + } else if (!existsSync(hookPath)) { out.push(warn("Claude plugin hooks", "manifest hooks path missing or invalid")); } else { const manifest = readJson(hookPath); diff --git a/test/channels.test.js b/test/channels.test.js index 0c18218..1d5c989 100644 --- a/test/channels.test.js +++ b/test/channels.test.js @@ -48,3 +48,20 @@ test("plugin hooks wire guards from the plugin root", () => { assert.match(hooks, /CLAUDE_PLUGIN_ROOT/); assert.match(hooks, /global\/guards\/protect-paths\.sh/); }); + +// Claude Code loads the standard hooks/hooks.json automatically. Declaring it in +// `manifest.hooks` too registers the same file twice, and the loader rejects the +// WHOLE plugin ("Duplicate hooks file detected") — skills, agents, guards and the +// MCP server all vanish. manifest.hooks is only for ADDITIONAL hook files. +test("plugin manifest does not re-declare the standard hooks file", () => { + const plugin = readJson(".claude-plugin/plugin.json"); + const declared = [plugin.hooks ?? []].flat(); + for (const h of declared) { + assert.notMatch( + h, + /^\.\/hooks\/hooks\.json$/, + "hooks/hooks.json loads automatically; declaring it fails plugin load", + ); + } + assert.ok(existsSync(join(root, "hooks/hooks.json")), "the auto-loaded hooks file still exists"); +});