Windows: hook commands with embedded quotes never execute (cmd /C outer-quote wrap in command_runner.rs) but hooks report Completed
Environment
- codex-cli 0.147.0 on Windows 11 x64 (the spawn form below is also present on current
main) - Project hooks in
.codex/hooks.jsonusingcommandWindows
Summary
On Windows, build_command in codex-rs/hooks/src/engine/command_runner.rs spawns hook commands as cmd.exe /C "<command_line>" — the entire hook command line is wrapped in a literal pair of double quotes via raw_arg (shipped in #33926 to fix #32402).
That fix covers a leading quoted executable path ("C:\path with spaces\hook.cmd" notify), but it regresses any hook command that contains an embedded quoted segment — for example the natural way to write a PowerShell hook:
powershell.exe -NoProfile -NonInteractive -Command "<payload>"
cmd.exe mis-parses the outer-wrapped line, so the payload never executes. Depending on the exact token stream Codex still prints hook: <Event> Completed, making the failure completely silent (this is very likely the mechanism behind #33564 as well).
Minimal repro
- Configure and trust a project
UserPromptSubmithook with:
"commandWindows": "powershell.exe -NoProfile -NonInteractive -Command \"Set-Content -Path '%USERPROFILE%\\hook-canary.txt' -Value ran\""
- Trigger the event.
- Codex reports
hook: UserPromptSubmit Completed, buthook-canary.txtis never created. No spawn result, exit code, stdout, or stderr is persisted anywhere I could find.
Exact spawn-form repro (outside Codex)
This is exactly what Codex executes since #33926 (cmd.exe /C + raw_arg("\"{command_line}\"")):
cmd.exe /C "powershell.exe -NoProfile -NonInteractive -Command "Set-Content -Path 'D:\Temp\canary.txt' -Value ran""
Result: exit 1, 系统找不到指定的路径。 (path-not-found from cmd's mis-parse), file never created.
Control cases that both work correctly:
- direct spawn:
powershell.exe -NoProfile -NonInteractive -Command "Set-Content -Path 'D:\Temp\canary.txt' -Value ran"→ exit 0, file created cmd.exe /Cwithout the outer quote wrap:cmd.exe /C powershell.exe -NoProfile -NonInteractive -Command "Set-Content -Path 'D:\Temp\canary.txt' -Value ran"→ exit 0, file created
So the defect is specifically the outer-quote wrapping, not the command itself and not PowerShell.
Expected behavior
- Pass the hook command line to
cmd.exe /Cverbatim (viaraw_arg, without adding an outer quote pair), or otherwise preserve embedded quotes correctly, so commands like the PowerShell form above execute as written. - When the hook process fails to launch or exits non-zero, do not report
Completed; persist the spawn result, exit code, stdout, and stderr so silent no-ops are diagnosable (also requested in #33564).
Workaround
Use a quote-free, space-free command line and move all real logic into a batch file:
"commandWindows": "cmd.exe /c C:\\path\\to\\hook-wrapper.cmd"
Verified working end-to-end on codex-cli 0.147.0 (hook output JSON is parsed and additionalContext attaches correctly once the command actually executes).
Refs
- #32402 — original quoted-path failure (closed by #33926)
- #33926 — introduced the outer-quote wrap that causes this embedded-quote failure
- #33564 — Desktop: trusted hook "completes" but handler never reaches script entry; likely the same mechanism
2 Comments
I have a fix for this
Confirmed on
main@ 1f41cc5d92 — the outer-wrap is atcodex-rs/hooks/src/engine/command_runner.rs#L384and#L393:command.raw_arg(format!(r#""{command_line}""#))whenever the shell iscmd-style. Your regression analysis matches: that wrap fixes the leading-quoted-path case from #32402 but breaks embedded quotes, because plaincmd /C "<line>"applies cmd's legacy quote-stripping heuristics to the whole line.The standard fix is the documented
cmdpattern for exactly this: spawn ascmd /S /C "<line>". With/S, cmd strips exactly the first and last quote and treats everything between them verbatim — which handles both the #32402 leading-quoted executable and embedded-Command "<payload>"segments, no escaping games. One-token change at the tworaw_argsites (add/Sbefore/Cindefault_shell_commandand honor it in the/c-detection branch).On the silent
Completed: same gap flagged in #38295 —HandlerRunResultcaptures exit/stdout/stderr but the rendered status doesn't surface them, so a mis-parsed spawn that exits 0-ish reads as success. Surfacing exit code + stderr tail fixes the observability half for both issues (and likely #33564, as you note).