diff --git a/codex-rs/core/src/shell_env_file.rs b/codex-rs/core/src/shell_env_file.rs index 1511d7cfbf..88969e3364 100644 --- a/codex-rs/core/src/shell_env_file.rs +++ b/codex-rs/core/src/shell_env_file.rs @@ -7,6 +7,7 @@ use anyhow::Result; use anyhow::anyhow; use anyhow::bail; use codex_protocol::ThreadId; +use codex_protocol::shell_environment::CLAUDE_ENV_FILE_ENV_VAR; use codex_protocol::shell_environment::CODEX_ENV_FILE_ENV_VAR; use codex_protocol::shell_environment::CODEX_THREAD_ID_ENV_VAR; use tempfile::TempPath; @@ -52,11 +53,9 @@ impl ShellEnvFile { } pub(crate) fn insert_path_into_env(&self, env: &mut HashMap) { - insert_env_var( - env, - CODEX_ENV_FILE_ENV_VAR.to_string(), - self.path().to_string_lossy().to_string(), - ); + let path = self.path().to_string_lossy().to_string(); + insert_env_var(env, CODEX_ENV_FILE_ENV_VAR.to_string(), path.clone()); + insert_env_var(env, CLAUDE_ENV_FILE_ENV_VAR.to_string(), path); } /// Sources the hook-writable env file once and stores the resulting @@ -65,8 +64,8 @@ impl ShellEnvFile { /// The temp file remains an input channel for SessionStart hooks, but later /// commands receive only captured variable changes. Running both a baseline /// environment dump and a sourced dump lets shell syntax such as command - /// substitution behave naturally without keeping `CODEX_ENV_FILE` available - /// after hook execution. + /// substitution behave naturally without keeping env-file path variables + /// available after hook execution. pub(crate) async fn capture_exports( &self, shell: &Shell, @@ -79,6 +78,7 @@ impl ShellEnvFile { let mut capture_env = base_env.clone(); remove_env_var(&mut capture_env, CODEX_ENV_FILE_ENV_VAR); + remove_env_var(&mut capture_env, CLAUDE_ENV_FILE_ENV_VAR); self.insert_path_into_env(&mut capture_env); let baseline = self @@ -135,6 +135,7 @@ impl ShellEnvFile { insert_env_var(env, CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id); } remove_env_var(env, CODEX_ENV_FILE_ENV_VAR); + remove_env_var(env, CLAUDE_ENV_FILE_ENV_VAR); } } @@ -317,6 +318,7 @@ fn diff_env( fn ignored_capture_key(key: &str) -> bool { [ CODEX_ENV_FILE_ENV_VAR, + CLAUDE_ENV_FILE_ENV_VAR, CODEX_THREAD_ID_ENV_VAR, "PWD", "OLDPWD", diff --git a/codex-rs/core/src/shell_env_file_tests.rs b/codex-rs/core/src/shell_env_file_tests.rs index 6fcff1d223..be03da5f2e 100644 --- a/codex-rs/core/src/shell_env_file_tests.rs +++ b/codex-rs/core/src/shell_env_file_tests.rs @@ -39,6 +39,7 @@ async fn shell_env_file_applies_exports_without_exposing_writable_path() -> Resu export CODEX_SESSION_START_TEST='from-session-start' export PATH=\"/plugin/bin:$PATH\" export CODEX_ENV_FILE='/tmp/poison' +export CLAUDE_ENV_FILE='/tmp/poison' export CODEX_THREAD_ID='poisoned-thread' export EXPLICIT_OVERRIDE='from-hook' ", @@ -53,6 +54,10 @@ export EXPLICIT_OVERRIDE='from-hook' CODEX_ENV_FILE_ENV_VAR.to_string(), env_file.path().display().to_string(), ); + env.insert( + CLAUDE_ENV_FILE_ENV_VAR.to_string(), + env_file.path().display().to_string(), + ); let explicit_env_overrides = HashMap::from([("EXPLICIT_OVERRIDE".to_string(), "from-policy".to_string())]); @@ -181,6 +186,7 @@ Write-Output "hidden" $env:CODEX_SESSION_START_TEST = "from-session-start" $env:COMMAND_EXPRESSION = ("unsafe").ToUpperInvariant() $env:CODEX_ENV_FILE = "C:\poison" +$env:CLAUDE_ENV_FILE = "C:\poison" $env:CODEX_THREAD_ID = "poisoned-thread" $env:EXPLICIT_OVERRIDE = "from-hook" Remove-Item Env:REMOVED_BY_HOOK -ErrorAction SilentlyContinue @@ -196,6 +202,10 @@ Remove-Item Env:REMOVED_BY_HOOK -ErrorAction SilentlyContinue CODEX_ENV_FILE_ENV_VAR.to_string(), env_file.path().display().to_string(), ); + env.insert( + CLAUDE_ENV_FILE_ENV_VAR.to_string(), + env_file.path().display().to_string(), + ); let explicit_env_overrides = HashMap::from([("EXPLICIT_OVERRIDE".to_string(), "from-policy".to_string())]); diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index db72fe0445..c314ec0674 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -783,7 +783,7 @@ fn write_session_start_hook_exporting_env(home: &Path) -> Result<()> { let session_start_script = r#"import os from pathlib import Path -env_file = Path(os.environ["CODEX_ENV_FILE"]) +env_file = Path(os.environ["CLAUDE_ENV_FILE"]) with env_file.open("a", encoding="utf-8") as handle: handle.write("export CODEX_SESSION_START_TEST='from-session-start'\n") "#; @@ -791,7 +791,7 @@ with env_file.open("a", encoding="utf-8") as handle: let pre_tool_use_script = r#"import os from pathlib import Path -env_file = os.environ.get("CODEX_ENV_FILE") +env_file = os.environ.get("CODEX_ENV_FILE") or os.environ.get("CLAUDE_ENV_FILE") if env_file: with Path(env_file).open("a", encoding="utf-8") as handle: handle.write("export CODEX_SESSION_START_TEST='leaked-to-pre-tool-use'\n") @@ -2717,7 +2717,7 @@ async fn assert_session_start_env_file_reaches_bash_surface( let server = start_mock_server().await; let call_id = format!("session-start-env-file-{}", surface.slug()); - let command = "printf '%s|%s' \"$CODEX_SESSION_START_TEST\" \"${CODEX_ENV_FILE:+configured}\""; + let command = "printf '%s|%s|%s' \"$CODEX_SESSION_START_TEST\" \"${CODEX_ENV_FILE:+configured}\" \"${CLAUDE_ENV_FILE:+configured}\""; let tool_call = match surface { BashRewriteSurface::ExecCommand => ev_function_call( &call_id, @@ -2773,12 +2773,12 @@ async fn assert_session_start_env_file_reaches_bash_surface( "expected hook-exported value in {surface:?} output: {output}" ); assert!( - output.contains("from-session-start|"), - "expected CODEX_ENV_FILE to stay hidden from {surface:?} command: {output}" + output.contains("from-session-start||"), + "expected env-file paths to stay hidden from {surface:?} command: {output}" ); assert!( !output.contains("leaked-to-pre-tool-use"), - "PreToolUse should not receive CODEX_ENV_FILE: {output}" + "PreToolUse should not receive env-file paths: {output}" ); Ok(()) @@ -2795,7 +2795,7 @@ async fn session_start_env_file_exports_reach_exec_command() -> Result<()> { } #[tokio::test] -async fn shell_command_hides_codex_env_file_when_hooks_are_disabled() -> Result<()> { +async fn shell_command_hides_env_file_paths_when_hooks_are_disabled() -> Result<()> { skip_if_no_network!(Ok(())); skip_if_windows!(Ok(())); @@ -2810,7 +2810,7 @@ async fn shell_command_hides_codex_env_file_when_hooks_are_disabled() -> Result< call_id, "shell_command", &serde_json::to_string(&serde_json::json!({ - "command": "printf '%s' \"${CODEX_ENV_FILE:+configured}\"", + "command": "printf '%s|%s' \"${CODEX_ENV_FILE:+configured}\" \"${CLAUDE_ENV_FILE:+configured}\"", "login": false, }))?, ), diff --git a/codex-rs/hooks/src/engine/command_runner.rs b/codex-rs/hooks/src/engine/command_runner.rs index 3eac1d0ecf..27fb1b3a1c 100644 --- a/codex-rs/hooks/src/engine/command_runner.rs +++ b/codex-rs/hooks/src/engine/command_runner.rs @@ -8,6 +8,7 @@ use tokio::process::Command; use tokio::time::timeout; use codex_protocol::protocol::HookEventName; +use codex_protocol::shell_environment::CLAUDE_ENV_FILE_ENV_VAR; use codex_protocol::shell_environment::CODEX_ENV_FILE_ENV_VAR; use super::CommandShell; @@ -116,8 +117,9 @@ fn build_command(shell: &CommandShell, handler: &ConfiguredHandler) -> Command { command.arg(&handler.command); } command.envs(&handler.env); - // Only SessionStart hooks receive CODEX_ENV_FILE. + // Only SessionStart hooks receive session env-file paths. command.env_remove(CODEX_ENV_FILE_ENV_VAR); + command.env_remove(CLAUDE_ENV_FILE_ENV_VAR); if handler.event_name == HookEventName::SessionStart { command.envs(&shell.env); } @@ -153,6 +155,7 @@ mod tests { use pretty_assertions::assert_eq; use tokio::process::Command; + use super::CLAUDE_ENV_FILE_ENV_VAR; use super::CODEX_ENV_FILE_ENV_VAR; use super::CommandShell; use super::ConfiguredHandler; @@ -162,10 +165,16 @@ mod tests { CommandShell { program: "hook-shell".to_string(), args: Vec::new(), - env: HashMap::from([( - CODEX_ENV_FILE_ENV_VAR.to_string(), - "session-owned-env-file".to_string(), - )]), + env: HashMap::from([ + ( + CODEX_ENV_FILE_ENV_VAR.to_string(), + "session-owned-env-file".to_string(), + ), + ( + CLAUDE_ENV_FILE_ENV_VAR.to_string(), + "session-owned-env-file".to_string(), + ), + ]), } } @@ -192,19 +201,24 @@ mod tests { } #[test] - fn non_session_start_hook_masks_inherited_codex_env_file() { + fn non_session_start_hook_masks_inherited_env_file_paths() { let command = build_command(&shell(), &handler(HookEventName::PreToolUse)); assert_eq!(command_env(&command, CODEX_ENV_FILE_ENV_VAR), Some(None)); + assert_eq!(command_env(&command, CLAUDE_ENV_FILE_ENV_VAR), Some(None)); } #[test] - fn session_start_hook_receives_session_owned_codex_env_file() { + fn session_start_hook_receives_session_owned_env_file_paths() { let command = build_command(&shell(), &handler(HookEventName::SessionStart)); assert_eq!( command_env(&command, CODEX_ENV_FILE_ENV_VAR), Some(Some(OsStr::new("session-owned-env-file"))) ); + assert_eq!( + command_env(&command, CLAUDE_ENV_FILE_ENV_VAR), + Some(Some(OsStr::new("session-owned-env-file"))) + ); } } diff --git a/codex-rs/hooks/src/events/session_start.rs b/codex-rs/hooks/src/events/session_start.rs index 18f17e7486..d3ed583771 100644 --- a/codex-rs/hooks/src/events/session_start.rs +++ b/codex-rs/hooks/src/events/session_start.rs @@ -7,6 +7,7 @@ use codex_protocol::protocol::HookOutputEntry; use codex_protocol::protocol::HookOutputEntryKind; use codex_protocol::protocol::HookRunStatus; use codex_protocol::protocol::HookRunSummary; +use codex_protocol::shell_environment::CLAUDE_ENV_FILE_ENV_VAR; use codex_protocol::shell_environment::CODEX_ENV_FILE_ENV_VAR; use codex_utils_absolute_path::AbsolutePathBuf; @@ -181,9 +182,10 @@ pub(crate) async fn run( }; let results = if event_name == HookEventName::SessionStart - && shell.env.contains_key(CODEX_ENV_FILE_ENV_VAR) + && (shell.env.contains_key(CODEX_ENV_FILE_ENV_VAR) + || shell.env.contains_key(CLAUDE_ENV_FILE_ENV_VAR)) { - // Serialize hooks that share CODEX_ENV_FILE to preserve configured order. + // Serialize hooks that share the session env file to preserve configured order. let mut results = Vec::with_capacity(matched.len()); for (completion_order, handler) in matched.into_iter().enumerate() { let result = run_command(shell, &handler, &input_json, request.cwd.as_path()).await; diff --git a/codex-rs/protocol/src/shell_environment.rs b/codex-rs/protocol/src/shell_environment.rs index 89f3bf9ca5..00394e0b44 100644 --- a/codex-rs/protocol/src/shell_environment.rs +++ b/codex-rs/protocol/src/shell_environment.rs @@ -4,6 +4,7 @@ use crate::config_types::ShellEnvironmentPolicyInherit; use std::collections::HashMap; pub const CODEX_ENV_FILE_ENV_VAR: &str = "CODEX_ENV_FILE"; +pub const CLAUDE_ENV_FILE_ENV_VAR: &str = "CLAUDE_ENV_FILE"; pub const CODEX_THREAD_ID_ENV_VAR: &str = "CODEX_THREAD_ID"; /// Construct a shell environment from the supplied process environment and