Skip to content

Commit 6743c04

Browse files
marikkan-microsoftCopilotDietrich Gebert
authored
fix(hooks): normalize a backslashed plugin root before loading hook scripts (#646) (#752)
Under WSL2 the host expands the plugin root with Windows separators (\home\user\...). That string is not absolute on Linux, so node resolves the hook relative to the current working directory and every session start and every prompt dies with MODULE_NOT_FOUND before any ponytail code runs. Normalise the separators in the bash commands so the path reaching node is a valid POSIX path. copilot-hooks.json keeps bash and powershell in separate fields, so bash-only expansion is safe here; the shared `command` field in claude-codex-hooks.json still has to run under PowerShell and is left alone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Dietrich Gebert <dietrichgebert@gmail.com>
1 parent 4a40c2a commit 6743c04

3 files changed

Lines changed: 25 additions & 17 deletions

File tree

‎hooks/claude-codex-hooks.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"hooks": [
77
{
88
"type": "command",
9-
"command": "node -e \"require(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT, 'hooks/ponytail-activate.js'))\"",
9+
"command": "node -e \"require(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT.replaceAll(String.fromCharCode(92), '/'), 'hooks/ponytail-activate.js'))\"",
1010
"timeout": 5,
1111
"statusMessage": "Loading ponytail mode..."
1212
}
@@ -18,7 +18,7 @@
1818
"hooks": [
1919
{
2020
"type": "command",
21-
"command": "node -e \"require(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT, 'hooks/ponytail-subagent.js'))\"",
21+
"command": "node -e \"require(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT.replaceAll(String.fromCharCode(92), '/'), 'hooks/ponytail-subagent.js'))\"",
2222
"timeout": 5,
2323
"statusMessage": "Loading ponytail mode..."
2424
}
@@ -30,7 +30,7 @@
3030
"hooks": [
3131
{
3232
"type": "command",
33-
"command": "node -e \"require(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT, 'hooks/ponytail-mode-tracker.js'))\"",
33+
"command": "node -e \"require(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT.replaceAll(String.fromCharCode(92), '/'), 'hooks/ponytail-mode-tracker.js'))\"",
3434
"timeout": 5,
3535
"statusMessage": "Tracking ponytail mode..."
3636
}

‎hooks/copilot-hooks.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
"sessionStart": [
55
{
66
"type": "command",
7-
"bash": "node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT, 'hooks/ponytail-activate.js'))\" || exit 0",
8-
"powershell": "if (Get-Command node -ErrorAction SilentlyContinue) { node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT, 'hooks/ponytail-activate.js'))\" }",
7+
"bash": "node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT.replaceAll(String.fromCharCode(92), '/'), 'hooks/ponytail-activate.js'))\" || exit 0",
8+
"powershell": "if (Get-Command node -ErrorAction SilentlyContinue) { node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT.replaceAll(String.fromCharCode(92), '/'), 'hooks/ponytail-activate.js'))\" }",
99
"timeoutSec": 5
1010
}
1111
],
1212
"userPromptSubmitted": [
1313
{
1414
"type": "command",
15-
"bash": "node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT, 'hooks/ponytail-mode-tracker.js'))\" || exit 0",
16-
"powershell": "if (Get-Command node -ErrorAction SilentlyContinue) { node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT, 'hooks/ponytail-mode-tracker.js'))\" }",
15+
"bash": "node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT.replaceAll(String.fromCharCode(92), '/'), 'hooks/ponytail-mode-tracker.js'))\" || exit 0",
16+
"powershell": "if (Get-Command node -ErrorAction SilentlyContinue) { node -e \"require(require('node:path').join(process.env.PLUGIN_ROOT.replaceAll(String.fromCharCode(92), '/'), 'hooks/ponytail-mode-tracker.js'))\" }",
1717
"timeoutSec": 5
1818
}
1919
]

‎tests/hooks.test.js‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,22 +80,30 @@ let result;
8080
// The shared Claude/Codex command must stay guard-free: VS Code runs it in
8181
// Windows PowerShell, which cannot parse `||` (see hooks-windows.test.js), so
8282
// it only has to run clean with node.
83+
// WSL2 can hand hooks a backslashed root (\home\user\...); the commands turn it
84+
// back into a POSIX path, so both shapes must load the script (#646).
85+
const roots = [root, root.split(path.sep).join('\\')];
86+
8387
for (const command of collectManifestCommands('hooks/claude-codex-hooks.json', 'command')) {
84-
result = runShell(command, { ...process.env, HOME: home, USERPROFILE: home, CLAUDE_PLUGIN_ROOT: root });
85-
assert.equal(result.status, 0, result.stderr);
86-
assert.equal(result.stderr, '', command);
88+
for (const pluginRoot of roots) {
89+
result = runShell(command, { ...process.env, HOME: home, USERPROFILE: home, CLAUDE_PLUGIN_ROOT: pluginRoot });
90+
assert.equal(result.status, 0, result.stderr);
91+
assert.equal(result.stderr, '', command);
92+
}
8793
}
8894

8995
// Copilot CLI has a separate bash field, so it can exit 0 without node (#645).
9096
// `|| exit 0` also hides a broken hook, so the with-node run must leave stderr empty.
9197
for (const command of collectManifestCommands('hooks/copilot-hooks.json', 'bash')) {
92-
const env = { HOME: home, USERPROFILE: home, PLUGIN_ROOT: root, COPILOT_PLUGIN_DATA: path.join(temp, 'copilot-manifest-data') };
93-
result = runShell(command, { ...process.env, ...env });
94-
assert.equal(result.status, 0, result.stderr);
95-
assert.equal(result.stderr, '', command);
96-
97-
result = runShell(command, { ...env, PATH: '' });
98-
assert.equal(result.status, 0, result.stderr);
98+
for (const pluginRoot of roots) {
99+
const env = { HOME: home, USERPROFILE: home, PLUGIN_ROOT: pluginRoot, COPILOT_PLUGIN_DATA: path.join(temp, 'copilot-manifest-data') };
100+
result = runShell(command, { ...process.env, ...env });
101+
assert.equal(result.status, 0, result.stderr);
102+
assert.equal(result.stderr, '', command);
103+
104+
result = runShell(command, { ...env, PATH: '' });
105+
assert.equal(result.status, 0, result.stderr);
106+
}
99107
}
100108

101109
// USERPROFILE alongside HOME: os.homedir() reads USERPROFILE on Windows, HOME on POSIX.

0 commit comments

Comments
 (0)