Preserve command cwd in permission and post-tool hooks (#57203)

## Why

`PermissionRequest` and `PostToolUse` hook inputs could report the turn's working directory instead of the local command's execution directory. This also affected commands whose results were collected in a later turn after the working directory changed.

## What changed

- Report the local command's execution directory in hook input `cwd`, consistent with `PreToolUse`.
- Preserve the original directory through `write_stdin` approvals and delayed completion, including network permission requests.
- Retain the host directory for remote commands and keep hook process working directories unchanged.

## Testing

Add coverage for relative and absolute workdirs, approval denials, command rewrites, completion and stdin approval across turns, remote command fallback, network approvals, and Unix and Windows path formatting.

GitOrigin-RevId: ef5dad2a0634dda9edb2f841ef4fe49ae0d1187d
This commit is contained in:
Andrei Eternal
2026-10-26 19:21:08 +00:00
committed by copyberry
parent 536d1f5b68
commit beb34ce8d0
26 changed files with 598 additions and 32 deletions
@@ -212,6 +212,7 @@ async fn exec_command_with_tty(
exit_code,
original_token_count: Some(original_token_count),
hook_command: Some(cmd.to_string()),
tool_cwd: None,
})
}
@@ -266,6 +266,7 @@ struct PreparedProcessHandles {
network_approval: Option<DeferredNetworkApproval>,
call_id: String,
hook_command: String,
tool_cwd: Option<PathUri>,
process_id: i32,
tty: bool,
}
@@ -874,6 +875,8 @@ impl UnifiedExecProcessManager {
exit_code,
original_token_count: Some(original_token_count),
hook_command: Some(request.hook_command.clone()),
tool_cwd: (!request.turn_environment.environment.is_remote())
.then(|| request.cwd.clone()),
};
Ok(response)
@@ -996,6 +999,7 @@ impl UnifiedExecProcessManager {
network_approval,
call_id,
hook_command,
tool_cwd,
process_id,
tty,
..
@@ -1126,6 +1130,7 @@ impl UnifiedExecProcessManager {
exit_code,
original_token_count: Some(original_token_count),
hook_command: Some(hook_command),
tool_cwd,
};
let should_emit_interaction = !request.input.is_empty() || response.process_id.is_some();
@@ -1230,6 +1235,10 @@ impl UnifiedExecProcessManager {
network_approval: entry.network_approval.clone(),
call_id: entry.call_id.clone(),
hook_command: entry.hook_command.clone(),
// Capture the original process's locality before it can be removed
// from the store. A later turn may select a different environment.
tool_cwd: (entry.environment_id == codex_exec_server::LOCAL_ENVIRONMENT_ID)
.then(|| entry.cwd.clone()),
process_id: entry.process_id,
tty: entry.tty,
})