Fix quoted hook commands on Windows (#33926)

## Why

Windows hook commands whose executable paths contain spaces fail when their
outer quotes are escaped during argument construction.

## What changed

- Pass hook command strings as raw arguments to the default Windows shell.
- Do the same for configured shells that use the case-insensitive `/c` flag,
  while retaining normal argument handling for other shells.

## Testing

Add a Windows regression test that runs a quoted `.cmd` hook from a directory
with spaces through both the default shell and an explicit `COMSPEC /c` shell.

GitOrigin-RevId: 0e7ce44203e01d3823daaaec750c2aeadfcd34b7
This commit is contained in:
Abhinav
2026-07-17 23:24:26 +00:00
committed by copyberry
parent f896ab9f25
commit 3ac476bed2
2 changed files with 80 additions and 0 deletions
@@ -168,9 +168,22 @@ fn build_command(shell: &CommandShell, handler: &ConfiguredHandler) -> Command {
Command::new(&shell.program)
};
if shell.program.is_empty() {
#[cfg(windows)]
command.raw_arg(format!(r#""{}""#, handler.command));
#[cfg(not(windows))]
command.arg(&handler.command);
} else {
command.args(&shell.args);
#[cfg(windows)]
if shell.args.iter().any(|arg| arg.eq_ignore_ascii_case("/c")) {
command.raw_arg(format!(r#""{}""#, handler.command));
} else {
command.arg(&handler.command);
}
#[cfg(not(windows))]
command.arg(&handler.command);
}
command.envs(&handler.env);
@@ -194,3 +207,7 @@ fn default_shell_command() -> Command {
command
}
}
#[cfg(all(test, windows))]
#[path = "command_runner_tests.rs"]
mod tests;
@@ -0,0 +1,63 @@
use std::collections::HashMap;
use std::fs;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
use super::CommandShell;
use super::ConfiguredHandler;
use super::run_command;
#[tokio::test]
async fn cmd_shell_runs_quoted_hook_command_path() {
let temp = tempdir().expect("create temp dir");
let hook_dir = temp.path().join("hook with spaces");
fs::create_dir(&hook_dir).expect("create hook dir");
let hook_path = hook_dir.join("hook.cmd");
fs::write(
&hook_path,
"@echo off\r\nif not \"%~1\"==\"notify\" exit /B 7\r\necho hook-ran\r\n",
)
.expect("write hook command");
let source_path =
AbsolutePathBuf::try_from(hook_path.clone()).expect("absolute hook command path");
let handler = ConfiguredHandler {
event_name: HookEventName::SessionStart,
matcher: None,
command: format!(r#""{}" notify"#, hook_path.display()),
timeout_sec: 10,
status_message: None,
source_path,
source: HookSource::User,
display_order: 0,
env: HashMap::new(),
};
let shells = [
CommandShell {
program: String::new(),
args: Vec::new(),
},
CommandShell {
program: std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()),
args: vec!["/c".to_string()],
},
];
for shell in shells {
let result = run_command(
&shell,
&handler,
/*configured_order*/ 0,
"{}",
temp.path(),
)
.await;
assert_eq!(result.exit_code, Some(0), "stderr: {}", result.stderr);
assert_eq!(result.stdout.trim(), "hook-ran");
assert!(result.error.is_none());
}
}