diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 8559852d..6f044d9b 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/setup.sh", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; \"$_R/scripts/setup.sh\"", "timeout": 300 } ] @@ -19,7 +19,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\"", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/smart-install.js\"", "timeout": 300 } ] @@ -29,12 +29,12 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" start", "timeout": 60 }, { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code context", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code context", "timeout": 60 } ] @@ -45,7 +45,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-init", "timeout": 60 } ] @@ -57,7 +57,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code observation", "timeout": 120 } ] @@ -68,12 +68,12 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code summarize", "timeout": 120 }, { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-complete", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-complete", "timeout": 30 } ] diff --git a/plugin/scripts/bun-runner.js b/plugin/scripts/bun-runner.js index 0de7c876..90ee0997 100644 --- a/plugin/scripts/bun-runner.js +++ b/plugin/scripts/bun-runner.js @@ -13,11 +13,36 @@ */ import { spawnSync, spawn } from 'child_process'; import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; +import { join, dirname, resolve } from 'path'; import { homedir } from 'os'; +import { fileURLToPath } from 'url'; const IS_WINDOWS = process.platform === 'win32'; +// Self-resolve plugin root when CLAUDE_PLUGIN_ROOT is not set by Claude Code. +// Upstream bug: anthropics/claude-code#24529 — Stop hooks (and on Linux, all hooks) +// don't receive CLAUDE_PLUGIN_ROOT, causing script paths to resolve to /scripts/... +// which doesn't exist. This fallback derives the plugin root from bun-runner.js's +// own filesystem location (this file lives in /scripts/). +const __bun_runner_dirname = dirname(fileURLToPath(import.meta.url)); +const RESOLVED_PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || resolve(__bun_runner_dirname, '..'); + +/** + * Fix script path arguments that were broken by empty CLAUDE_PLUGIN_ROOT. + * When CLAUDE_PLUGIN_ROOT is empty, "${CLAUDE_PLUGIN_ROOT}/scripts/foo.cjs" + * expands to "/scripts/foo.cjs" which doesn't exist. Detect this and rewrite + * the path using our self-resolved plugin root. + */ +function fixBrokenScriptPath(argPath) { + if (argPath.startsWith('/scripts/') && !existsSync(argPath)) { + const fixedPath = join(RESOLVED_PLUGIN_ROOT, argPath); + if (existsSync(fixedPath)) { + return fixedPath; + } + } + return argPath; +} + /** * Find Bun executable - checks PATH first, then common install locations */ @@ -80,6 +105,9 @@ if (args.length === 0) { process.exit(1); } +// Fix broken script paths caused by empty CLAUDE_PLUGIN_ROOT (#1215) +args[0] = fixBrokenScriptPath(args[0]); + const bunPath = findBun(); if (!bunPath) { diff --git a/tests/infrastructure/plugin-distribution.test.ts b/tests/infrastructure/plugin-distribution.test.ts index 45d37993..0bd58d6b 100644 --- a/tests/infrastructure/plugin-distribution.test.ts +++ b/tests/infrastructure/plugin-distribution.test.ts @@ -81,6 +81,22 @@ describe('Plugin Distribution - hooks.json Integrity', () => { } } }); + + it('should include CLAUDE_PLUGIN_ROOT fallback in all hook commands (#1215)', () => { + const hooksPath = path.join(projectRoot, 'plugin/hooks/hooks.json'); + const parsed = JSON.parse(readFileSync(hooksPath, 'utf-8')); + const expectedFallbackPath = '$HOME/.claude/plugins/marketplaces/thedotmack/plugin'; + + for (const [eventName, matchers] of Object.entries(parsed.hooks)) { + for (const matcher of matchers as any[]) { + for (const hook of matcher.hooks) { + if (hook.type === 'command') { + expect(hook.command).toContain(expectedFallbackPath); + } + } + } + } + }); }); describe('Plugin Distribution - package.json Files Field', () => {