diff --git a/src/main/agent-hooks/installer-utils.test.ts b/src/main/agent-hooks/installer-utils.test.ts index e68e3cc3554..40966c0f725 100644 --- a/src/main/agent-hooks/installer-utils.test.ts +++ b/src/main/agent-hooks/installer-utils.test.ts @@ -707,10 +707,7 @@ describe('wrapWindowsHookCommand', () => { describe('wrapWindowsCmdHookCommand', () => { it('returns the bare, directly-spawnable path for a cmd-safe managed script', () => { - // Why: Codex/Antigravity/Devin launch the command as a program (argv[0]), - // not via cmd.exe, so the launcher must be a single spawnable token — a bare - // .cmd path. A cmd-builtin `if …` launcher has argv[0] = `if`, which is - // unspawnable and fails every hook with exit 1 (#8430 regression). + // Direct-spawn consumers need a launchable argv[0], not a cmd builtin such as `if`. const scriptPath = 'C:\\Users\\alice\\.orca\\agent-hooks\\codex-hook.cmd' const command = wrapWindowsCmdHookCommand(scriptPath) expect(command).toBe(scriptPath) @@ -721,12 +718,7 @@ describe('wrapWindowsCmdHookCommand', () => { it.skipIf(process.platform !== 'win32')( 'resolves the launcher to a real executable file, not a shell fragment', () => { - // Regression guard for #8430: Codex/Antigravity/Devin spawn the launcher as - // a program (argv[0]), so it must be an existing, launchable file. The broken - // `if exist … (call …)` form had argv[0] = `if` — a cmd builtin, not a file — - // which is unspawnable and failed every hook. The bare path is the file. - // win32-only: the real temp path is cmd-safe only with backslashes; a POSIX - // tmpDir has `/`, which routes to the encoded fallback by design. + // POSIX temp paths contain `/`, which selects the encoded fallback instead. const scriptPath = join(tmpDir, 'codex-hook.cmd') writeFileSync(scriptPath, '@echo off\r\nexit /b 0\r\n', 'utf-8') const command = wrapWindowsCmdHookCommand(scriptPath) diff --git a/src/main/codex/codex-hook-definition.ts b/src/main/codex/codex-hook-definition.ts index 7b0c981a5ba..641a5fc1b08 100644 --- a/src/main/codex/codex-hook-definition.ts +++ b/src/main/codex/codex-hook-definition.ts @@ -74,7 +74,7 @@ export function getManagedCommand(scriptPath: string): string { if (process.platform !== 'win32') { return wrapPosixHookCommand(scriptPath) } - // Codex already hosts Windows hooks in PowerShell; a second interpreter only adds startup cost. + // Codex's default native Windows hook host is PowerShell; reuse it to avoid a second interpreter. return WINDOWS_CMD_SAFE_PATH.test(scriptPath) ? scriptPath : buildWindowsHookPowerShellCommand(scriptPath) diff --git a/src/main/codex/windows-hook-upgrade.test.ts b/src/main/codex/windows-hook-upgrade.test.ts index 738967753cd..203fdfb99c2 100644 --- a/src/main/codex/windows-hook-upgrade.test.ts +++ b/src/main/codex/windows-hook-upgrade.test.ts @@ -17,7 +17,11 @@ vi.mock('os', async (importOriginal) => ({ import { CodexHookService } from './hook-service' import { CODEX_EVENTS, CODEX_EVENT_LABEL, getManagedCommand } from './codex-hook-definition' import { readHooksJson, wrapWindowsHookCommand } from '../agent-hooks/installer-utils' -import { getCodexExplicitHomeHookSourcePath, upsertHookTrustEntries } from './config-toml-trust' +import { + computeTrustedHash, + getCodexExplicitHomeHookSourcePath, + upsertHookTrustEntries +} from './config-toml-trust' const homes = setupCodexHookHomes(homedirMock, getPathMock) @@ -57,27 +61,36 @@ describe.skipIf(process.platform !== 'win32')('Unicode Windows hook upgrade', () groupIndex: 0, handlerIndex: 0, command: oldCommand, - timeout: 10 + timeoutSec: 10 })) ) - const oldTrust = readFileSync(tomlPath, 'utf8') const service = new CodexHookService() expect(service.getStatus().state).not.toBe('installed') for (let pass = 0; pass < 2; pass++) { expect((await service.install()).state).toBe('installed') expect(service.getStatus().state).toBe('installed') const hooks = readHooksJson(configPath)?.hooks + const trust = readFileSync(tomlPath, 'utf8') for (const event of CODEX_EVENTS) { const commands = hooks?.[event]?.flatMap((group) => group.hooks ?? []) ?? [] expect( commands.filter((hook) => hook.command === getManagedCommand(scriptPath)) ).toHaveLength(1) expect(commands.some((hook) => hook.command === oldCommand)).toBe(false) + const entry = { + sourcePath: getCodexExplicitHomeHookSourcePath(configPath), + eventLabel: CODEX_EVENT_LABEL[event], + groupIndex: 0, + handlerIndex: 0, + command: getManagedCommand(scriptPath), + timeoutSec: 10 + } + expect(trust).toContain(computeTrustedHash(entry)) + expect(trust).not.toContain(computeTrustedHash({ ...entry, command: oldCommand })) } expect( hooks?.Stop?.some((group) => group.hooks?.some((hook) => hook.command === 'user-hook')) ).toBe(true) - expect(readFileSync(tomlPath, 'utf8')).not.toBe(oldTrust) expect(readFileSync(join(userHome, 'hooks.json'), 'utf8')).toBe(userConfig) } })