fix: add CLAUDE_PLUGIN_ROOT fallback for Stop hooks (#1215)
Upstream Claude Code bug (anthropics/claude-code#24529) leaves CLAUDE_PLUGIN_ROOT unset for Stop hooks on macOS and ALL hooks on Linux. Two-layer defense: 1. Shell-level: hooks.json commands now use inline fallback _R="${CLAUDE_PLUGIN_ROOT}"; [ -z "$_R" ] && _R="$HOME/..."; falling back to the known marketplace install path. 2. Script-level: bun-runner.js self-resolves plugin root from its own filesystem location via import.meta.url, and fixes broken /scripts/... paths that result from empty expansion. Added test to verify all hook commands include the fallback path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 300
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 300
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -29,12 +29,12 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 60
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 60
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 120
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -68,12 +68,12 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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
|
"timeout": 30
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,11 +13,36 @@
|
|||||||
*/
|
*/
|
||||||
import { spawnSync, spawn } from 'child_process';
|
import { spawnSync, spawn } from 'child_process';
|
||||||
import { existsSync, readFileSync } from 'fs';
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join, dirname, resolve } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32';
|
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 <plugin-root>/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
|
* Find Bun executable - checks PATH first, then common install locations
|
||||||
*/
|
*/
|
||||||
@@ -80,6 +105,9 @@ if (args.length === 0) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix broken script paths caused by empty CLAUDE_PLUGIN_ROOT (#1215)
|
||||||
|
args[0] = fixBrokenScriptPath(args[0]);
|
||||||
|
|
||||||
const bunPath = findBun();
|
const bunPath = findBun();
|
||||||
|
|
||||||
if (!bunPath) {
|
if (!bunPath) {
|
||||||
|
|||||||
@@ -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', () => {
|
describe('Plugin Distribution - package.json Files Field', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user