From e650ab46ca49f7421622e3ee530fdb0ae7bcc3cb Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Sat, 13 Jun 2026 22:22:06 +0000
Subject: [PATCH] core: honor remote environment cwd and shell

---
 .../handlers/unified_exec/exec_command.rs     |  7 ++-
 .../remote_env_windows_test.rs                | 45 +++++++++++--------
 codex-rs/protocol/src/protocol.rs             |  2 +
 3 files changed, 34 insertions(+), 20 deletions(-)

diff --git a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs
index 55d3befeeb68..1b58957a5849 100644
--- a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs
+++ b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs
@@ -151,9 +151,14 @@ impl ExecCommandHandler {
         let process_id = manager.allocate_process_id().await;
         let shell_mode =
             shell_mode_for_environment(&turn.unified_exec_shell_mode, environment.as_ref());
+        let shell = turn_environment
+            .shell
+            .clone()
+            .map(Arc::new)
+            .unwrap_or_else(|| session.user_shell());
         let resolved_command = get_command(
             &args,
-            session.user_shell(),
+            shell,
             &shell_mode,
             turn.config.permissions.allow_login_shell,
         )
diff --git a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs
index 3c352b901fc7..7393db233a66 100644
--- a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs
+++ b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs
@@ -7,6 +7,7 @@ use codex_features::Feature;
 use codex_protocol::models::PermissionProfile;
 use codex_protocol::protocol::AskForApproval;
 use codex_protocol::protocol::EventMsg;
+use codex_protocol::protocol::ExecCommandStatus;
 use codex_protocol::protocol::Op;
 use codex_protocol::protocol::TurnEnvironmentSelection;
 use codex_protocol::protocol::TurnEnvironmentSelections;
@@ -22,16 +23,17 @@ use core_test_support::test_codex::test_codex;
 use core_test_support::test_codex::turn_permission_fields;
 use core_test_support::wait_for_event;
 use codex_utils_path_uri::PathUri;
+use pretty_assertions::assert_eq;
 use serde_json::json;
 use tokio::io::AsyncBufReadExt;
 use tokio::io::BufReader;
 use wine_test_support::WineTestCommand;
 
 const CALL_ID: &str = "wine-cmd-smoke";
-const COMMAND: &str = "echo WINE_BAZEL_OK&&cd";
+const COMMAND: &str = r#"if ((Get-Location).Path -ne 'C:\windows') { exit 1 }"#;
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn windows_exec_server_rejects_non_native_cwd_uri() -> Result<()> {
+async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
     let executable = codex_utils_cargo_bin::cargo_bin("wine-windows-exec-server")?;
     let mut exec_server = WineTestCommand::new(executable)
         .env("CODEX_HOME", r"C:\codex-home")
@@ -91,7 +93,7 @@ async fn windows_exec_server_rejects_non_native_cwd_uri() -> Result<()> {
                 test.config.cwd.clone(),
                 vec![TurnEnvironmentSelection {
                     environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
-                    cwd: PathUri::from_abs_path(&test.config.cwd),
+                    cwd: PathUri::parse("file:///C:/windows")?,
                 }],
             );
 
@@ -122,39 +124,44 @@ async fn windows_exec_server_rejects_non_native_cwd_uri() -> Result<()> {
                 })
                 .await?;
 
-            let mut saw_exec_event = false;
+            let mut begin = None;
+            let mut end = None;
             loop {
                 match wait_for_event(&test.codex, |_| true).await {
                     EventMsg::ExecCommandBegin(event) if event.call_id == CALL_ID => {
-                        saw_exec_event = true
+                        begin = Some(event)
                     }
                     EventMsg::ExecCommandEnd(event) if event.call_id == CALL_ID => {
-                        saw_exec_event = true
+                        end = Some(event)
                     }
                     EventMsg::TurnComplete(_) => break,
                     _ => {}
                 }
             }
 
+            let begin = begin.context("exec_command should emit a begin event")?;
             assert!(
-                !saw_exec_event,
-                "a non-native cwd should be rejected before process lifecycle events",
+                begin.command.first().is_some_and(|command| command
+                    .to_ascii_lowercase()
+                    .ends_with("powershell.exe")),
+                "unexpected command: {:?}",
+                begin.command
             );
+            assert_eq!(
+                &begin.command[1..],
+                ["-NoProfile", "-Command", COMMAND]
+            );
+
+            let end = end.context("exec_command should emit an end event")?;
+            assert_eq!((end.exit_code, end.status), (0, ExecCommandStatus::Completed));
 
             let request = response_mock
                 .last_request()
-                .context("model should receive the rejected command output")?;
-            let (output, success) = request
+                .context("model should receive the command output")?;
+            let (_output, success) = request
                 .function_call_output_content_and_success(CALL_ID)
-                .context("rejected command output should be present")?;
-            let output = output.context("rejected command output should contain text")?;
-            assert!(
-                output.contains("exec-server rejected request (-32602)")
-                    && output.contains("cwd URI")
-                    && output.contains("is not valid on this exec-server host"),
-                "unexpected command output: {output:?}",
-            );
-            assert_ne!(success, Some(true));
+                .context("command output should be present")?;
+            assert_ne!(success, Some(false));
 
             Ok(())
         })
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index 900d167a091f..f938555b708e 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -106,6 +106,7 @@ pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
 pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";
 pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = "</realtime_conversation>";
 pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
+const LOCAL_ENVIRONMENT_ID: &str = "local";
 
 // TODO(anp): Replace `TurnEnvironmentSelection` with `PathUri` once path URIs carry environment
 // identifiers.
@@ -137,6 +138,7 @@ impl TurnEnvironmentSelections {
     fn sync_primary_environment_cwd(&mut self) {
         let legacy_fallback_cwd = PathUri::from_abs_path(&self.legacy_fallback_cwd);
         if let Some(turn_environment) = self.environments.first_mut()
+            && turn_environment.environment_id == LOCAL_ENVIRONMENT_ID
             && turn_environment.cwd != legacy_fallback_cwd
         {
             turn_environment.cwd = legacy_fallback_cwd;
