Windows: hook commands with embedded quotes never execute (cmd /C outer-quote wrap in command_runner.rs) but hooks report Completed

Open 💬 2 comments Opened Aug 12, 2026 by M1nt-Ch0c0

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.json using commandWindows

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

  1. Configure and trust a project UserPromptSubmit hook with:
"commandWindows": "powershell.exe -NoProfile -NonInteractive -Command \"Set-Content -Path '%USERPROFILE%\\hook-canary.txt' -Value ran\""
  1. Trigger the event.
  1. Codex reports hook: UserPromptSubmit Completed, but hook-canary.txt is 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 /C without 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

  1. Pass the hook command line to cmd.exe /C verbatim (via raw_arg, without adding an outer quote pair), or otherwise preserve embedded quotes correctly, so commands like the PowerShell form above execute as written.
  2. 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

View original on GitHub ↗

2 Comments

Kolgrim33 · 15 days ago

I have a fix for this

jdcodes1 · 9 days ago

Confirmed on main @ 1f41cc5d92 — the outer-wrap is at codex-rs/hooks/src/engine/command_runner.rs#L384 and #L393: command.raw_arg(format!(r#""{command_line}""#)) whenever the shell is cmd-style. Your regression analysis matches: that wrap fixes the leading-quoted-path case from #32402 but breaks embedded quotes, because plain cmd /C "<line>" applies cmd's legacy quote-stripping heuristics to the whole line.

The standard fix is the documented cmd pattern for exactly this: spawn as cmd /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 two raw_arg sites (add /S before /C in default_shell_command and honor it in the /c-detection branch).

On the silent Completed: same gap flagged in #38295 — HandlerRunResult captures 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).