diff --git a/codex-rs/core/src/shell_snapshot.rs b/codex-rs/core/src/shell_snapshot.rs index 29b50cb9e8..8c0cc1550d 100644 --- a/codex-rs/core/src/shell_snapshot.rs +++ b/codex-rs/core/src/shell_snapshot.rs @@ -334,7 +334,13 @@ export_lines=$(export -p | awk ' line=$0 name=line sub(/^(export|declare -x|typeset -x) /, "", name) - sub(/=.*/, "", name) + if (name ~ /^-T [A-Za-z_][A-Za-z0-9_]* [A-Za-z_][A-Za-z0-9_]*=/) { + # Zsh prints tied parameters like PATH/path as `export -T NAME tied=(...)`. + sub(/^-T /, "", name) + sub(/ .*/, "", name) + } else { + sub(/=.*/, "", name) + } if (name ~ /^(EXCLUDED_EXPORTS)$/) { next } diff --git a/codex-rs/core/src/shell_snapshot_tests.rs b/codex-rs/core/src/shell_snapshot_tests.rs index ff700ff7a6..8f0bdd15b3 100644 --- a/codex-rs/core/src/shell_snapshot_tests.rs +++ b/codex-rs/core/src/shell_snapshot_tests.rs @@ -184,6 +184,53 @@ fn bash_snapshot_preserves_multiline_exports() -> Result<()> { Ok(()) } +#[cfg(target_os = "macos")] +#[test] +fn zsh_snapshot_preserves_tied_path_export() -> Result<()> { + let dir = tempdir()?; + let tool_dir = dir.path().join("toolbin"); + let expected_path = format!("{}:/usr/bin:/bin", tool_dir.display()); + std::fs::write( + dir.path().join(".zshrc"), + format!("export PATH='{expected_path}'\n"), + )?; + + let output = Command::new("/bin/zsh") + .arg("-fc") + .arg(zsh_snapshot_script()) + .env("ZDOTDIR", dir.path()) + .output()?; + assert!(output.status.success()); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("export -T PATH path="), + "snapshot should include zsh tied PATH export; stdout={stdout:?}" + ); + + let snapshot_path = dir.path().join("snapshot.sh"); + std::fs::write(&snapshot_path, stdout.as_bytes())?; + + let validate = Command::new("/bin/zsh") + .arg("-fc") + .arg("PATH=/should/not/survive; . \"$1\"; print -r -- \"$PATH\"") + .arg("zsh") + .arg(&snapshot_path) + .output()?; + assert!( + validate.status.success(), + "snapshot validation failed: {}", + String::from_utf8_lossy(&validate.stderr) + ); + + assert_eq!( + String::from_utf8_lossy(&validate.stdout).trim(), + expected_path + ); + + Ok(()) +} + #[cfg(unix)] #[tokio::test] async fn try_new_creates_and_deletes_snapshot_file() -> Result<()> {