From 1f990855599afe630d1e4abbdfa5428776343767 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 11 May 2026 15:27:44 -0400 Subject: [PATCH] Fix(hooks): normalize Windows managed hook script paths (#3396) * fix: normalize windows managed hook script paths * chore: add changeset for codex hook paths * docs: hyphenate windows managed hooks changeset --- .changeset/codex-windows-hook-script-paths.md | 5 +++++ bin/install.js | 9 ++++++++- tests/bug-2979-hook-absolute-node.test.cjs | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .changeset/codex-windows-hook-script-paths.md diff --git a/.changeset/codex-windows-hook-script-paths.md b/.changeset/codex-windows-hook-script-paths.md new file mode 100644 index 000000000..83493221b --- /dev/null +++ b/.changeset/codex-windows-hook-script-paths.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3396 +--- +**Codex Windows-managed Node hooks now use double-quoted forward-slash script paths** - migrated hook commands no longer preserve POSIX single quotes that Windows treats as literal path characters. diff --git a/bin/install.js b/bin/install.js index b711f2aaa..26829d552 100755 --- a/bin/install.js +++ b/bin/install.js @@ -607,6 +607,12 @@ function formatHookCommandForShell(command, opts) { return platform === 'win32' ? `& ${command}` : command; } +function formatManagedHookScriptToken(scriptPath, opts) { + const platform = (opts && opts.platform) || process.platform; + if (platform !== 'win32') return null; + return JSON.stringify(scriptPath.replace(/\\/g, '/')); +} + function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) { if (!settings || !settings.hooks || !absoluteRunner) return false; if (!opts) opts = {}; @@ -680,7 +686,8 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) { if (platform !== 'win32' || hadPowerShellCallOperator) continue; } - h.command = formatHookCommandForShell(`${absoluteRunner} ${scriptToken}`, opts); + const safeScriptToken = formatManagedHookScriptToken(scriptPath, opts) || scriptToken; + h.command = formatHookCommandForShell(`${absoluteRunner} ${safeScriptToken}`, opts); changed = true; } } diff --git a/tests/bug-2979-hook-absolute-node.test.cjs b/tests/bug-2979-hook-absolute-node.test.cjs index e0e4a1332..7ae14e913 100644 --- a/tests/bug-2979-hook-absolute-node.test.cjs +++ b/tests/bug-2979-hook-absolute-node.test.cjs @@ -335,6 +335,26 @@ describe('Bug #2979 (#3002 CR): rewriteLegacyManagedNodeHookCommands rewrites ba const changed = rewriteLegacyManagedNodeHookCommands(settings, runner); assert.equal(changed, true); }); + + test('normalizes single-quoted Windows managed hook paths to double-quoted forward-slash paths (#3392)', () => { + const settings = { + hooks: { + PreToolUse: [{ + hooks: [{ + type: 'command', + command: "node 'C:\\Users\\me\\.codex\\hooks\\gsd-prompt-guard.js'", + }], + }], + }, + }; + const runner = '"C:/nvm4w/nodejs/node.exe"'; + const changed = rewriteLegacyManagedNodeHookCommands(settings, runner, { platform: 'win32' }); + assert.equal(changed, true); + assert.equal( + settings.hooks.PreToolUse[0].hooks[0].command, + '& "C:/nvm4w/nodejs/node.exe" "C:/Users/me/.codex/hooks/gsd-prompt-guard.js"', + ); + }); }); describe('Bug #2979 (#3002 CR): resolveNodeRunner returns null when execPath unavailable', () => {