fix: use PowerShell $env: syntax for Windows hook paths (#26)
On Windows the lifecycle hooks run via PowerShell, which does not expand cmd.exe-style %CLAUDE_PLUGIN_ROOT%. The path was passed literally, so the hook launcher could not find the script and both SessionStart and UserPromptSubmit failed with exit code 1 (issue #19). Switch the two commandWindows entries to $env:CLAUDE_PLUGIN_ROOT, keeping the working node + .js invocation. Add a regression test that rejects cmd.exe %VAR% syntax in commandWindows and asserts every hook command points at a script that actually ships in hooks/. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
92efc4a648
commit
147bcfd621
+2
-2
@@ -7,7 +7,7 @@
|
|||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-activate.js\"",
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-activate.js\"",
|
||||||
"commandWindows": "node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\ponytail-activate.js\"",
|
"commandWindows": "node \"$env:CLAUDE_PLUGIN_ROOT\\hooks\\ponytail-activate.js\"",
|
||||||
"timeout": 5,
|
"timeout": 5,
|
||||||
"statusMessage": "Loading ponytail mode..."
|
"statusMessage": "Loading ponytail mode..."
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\"",
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\"",
|
||||||
"commandWindows": "node \"%CLAUDE_PLUGIN_ROOT%\\hooks\\ponytail-mode-tracker.js\"",
|
"commandWindows": "node \"$env:CLAUDE_PLUGIN_ROOT\\hooks\\ponytail-mode-tracker.js\"",
|
||||||
"timeout": 5,
|
"timeout": 5,
|
||||||
"statusMessage": "Tracking ponytail mode..."
|
"statusMessage": "Tracking ponytail mode..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Regression test for issue #19: on Windows the lifecycle hooks run via
|
||||||
|
// PowerShell, which does NOT expand cmd.exe-style %VAR% — it needs $env:VAR.
|
||||||
|
// The hook also has to point at a script that actually ships in hooks/.
|
||||||
|
// This guards both failure modes: the original %CLAUDE_PLUGIN_ROOT% bug, and
|
||||||
|
// the "switch to a .ps1 that doesn't exist" mistake.
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..');
|
||||||
|
const HOOKS_JSON = 'hooks/hooks.json';
|
||||||
|
// cmd.exe variable syntax (%FOO%); PowerShell leaves it literal, breaking the path.
|
||||||
|
const CMD_VAR_SYNTAX = /%[A-Za-z_][A-Za-z0-9_]*%/;
|
||||||
|
// Pull the hooks/<script> a command launches, so we can check it exists.
|
||||||
|
const HOOK_SCRIPT = /hooks[\\/]([\w.-]+\.(?:js|mjs|cjs|ps1|sh))/;
|
||||||
|
|
||||||
|
// Read inside each case so a missing/malformed file fails as a clean assertion,
|
||||||
|
// not a load-time crash.
|
||||||
|
function commandHooks() {
|
||||||
|
const config = JSON.parse(fs.readFileSync(path.join(root, HOOKS_JSON), 'utf8'));
|
||||||
|
return Object.values(config.hooks)
|
||||||
|
.flat()
|
||||||
|
.flatMap((entry) => entry.hooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('every commandWindows uses PowerShell $env: syntax, not cmd.exe %VAR%', () => {
|
||||||
|
const windowsCommands = commandHooks()
|
||||||
|
.map((h) => h.commandWindows)
|
||||||
|
.filter(Boolean);
|
||||||
|
assert.ok(windowsCommands.length > 0, 'expected at least one commandWindows entry');
|
||||||
|
for (const cmd of windowsCommands) {
|
||||||
|
assert.doesNotMatch(cmd, CMD_VAR_SYNTAX, `commandWindows uses cmd.exe %VAR% (breaks under PowerShell): ${cmd}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every hook command points at a script that ships in hooks/', () => {
|
||||||
|
for (const hook of commandHooks()) {
|
||||||
|
for (const cmd of [hook.command, hook.commandWindows].filter(Boolean)) {
|
||||||
|
const match = cmd.match(HOOK_SCRIPT);
|
||||||
|
assert.ok(match, `cannot find a hooks/ script in command: ${cmd}`);
|
||||||
|
const script = path.join(root, 'hooks', match[1]);
|
||||||
|
assert.ok(fs.existsSync(script), `command references a missing hook script: ${match[1]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user