diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml
index c1d231f9e3..f0eadaf254 100644
--- a/.github/workflows/rust-ci.yml
+++ b/.github/workflows/rust-ci.yml
@@ -100,6 +100,8 @@ jobs:
id: test
continue-on-error: true
run: cargo test --all-features --target ${{ matrix.target }}
+ env:
+ RUST_BACKTRACE: 1
# Fail the job if any of the previous steps failed.
- name: verify all steps passed
diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js
index 818b362700..1bfb9f5d5d 100755
--- a/codex-cli/bin/codex.js
+++ b/codex-cli/bin/codex.js
@@ -16,14 +16,23 @@
*/
import { spawnSync } from "child_process";
+import fs from "fs";
import path from "path";
import { fileURLToPath, pathToFileURL } from "url";
// Determine whether the user explicitly wants the Rust CLI.
-const wantsNative =
- process.env.CODEX_RUST != null
+
+// __dirname equivalent in ESM
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// For the @native release of the Node module, the `use-native` file is added,
+// indicating we should default to the native binary. For other releases,
+// setting CODEX_RUST=1 will opt-in to the native binary, if included.
+const wantsNative = fs.existsSync(path.join(__dirname, "use-native")) ||
+ (process.env.CODEX_RUST != null
? ["1", "true", "yes"].includes(process.env.CODEX_RUST.toLowerCase())
- : false;
+ : false);
// Try native binary if requested.
if (wantsNative) {
@@ -63,10 +72,6 @@ if (wantsNative) {
throw new Error(`Unsupported platform: ${platform} (${arch})`);
}
- // __dirname equivalent in ESM
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
-
const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
const result = spawnSync(binaryPath, process.argv.slice(2), {
stdio: "inherit",
@@ -78,10 +83,6 @@ if (wantsNative) {
// Fallback: execute the original JavaScript CLI.
-// Determine this script's directory
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
// Resolve the path to the compiled CLI bundle
const cliPath = path.resolve(__dirname, "../dist/cli.js");
const cliUrl = pathToFileURL(cliPath).href;
diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh
index 00f355ca7d..5275627f6e 100755
--- a/codex-cli/scripts/install_native_deps.sh
+++ b/codex-cli/scripts/install_native_deps.sh
@@ -65,7 +65,7 @@ mkdir -p "$BIN_DIR"
# Until we start publishing stable GitHub releases, we have to grab the binaries
# from the GitHub Action that created them. Update the URL below to point to the
# appropriate workflow run:
-WORKFLOW_URL="https://github.com/openai/codex/actions/runs/15087655786"
+WORKFLOW_URL="https://github.com/openai/codex/actions/runs/15192425904"
WORKFLOW_ID="${WORKFLOW_URL##*/}"
ARTIFACTS_DIR="$(mktemp -d)"
diff --git a/codex-cli/scripts/stage_release.sh b/codex-cli/scripts/stage_release.sh
index fb641d35d9..9e251b9059 100755
--- a/codex-cli/scripts/stage_release.sh
+++ b/codex-cli/scripts/stage_release.sh
@@ -122,6 +122,7 @@ jq --arg version "$VERSION" \
if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then
./scripts/install_native_deps.sh "$TMPDIR" --full-native
+ touch "${TMPDIR}/bin/use-native"
else
./scripts/install_native_deps.sh "$TMPDIR"
fi
@@ -130,11 +131,12 @@ popd >/dev/null
echo "Staged version $VERSION for release in $TMPDIR"
-echo "Test Node:"
-echo " node ${TMPDIR}/bin/codex.js --help"
if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then
echo "Test Rust:"
- echo " CODEX_RUST=1 node ${TMPDIR}/bin/codex.js --help"
+ echo " node ${TMPDIR}/bin/codex.js --help"
+else
+ echo "Test Node:"
+ echo " node ${TMPDIR}/bin/codex.js --help"
fi
# Print final hint for convenience
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 5358065cd5..6408e8de6f 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -526,6 +526,7 @@ dependencies = [
"futures",
"landlock",
"libc",
+ "maplit",
"mcp-types",
"mime_guess",
"openssl-sys",
@@ -548,6 +549,7 @@ dependencies = [
"tree-sitter",
"tree-sitter-bash",
"uuid",
+ "wildmatch",
"wiremock",
]
@@ -4309,6 +4311,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "wildmatch"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd"
+
[[package]]
name = "winapi"
version = "0.3.9"
diff --git a/codex-rs/README.md b/codex-rs/README.md
index bedce9f22d..705d313071 100644
--- a/codex-rs/README.md
+++ b/codex-rs/README.md
@@ -222,6 +222,49 @@ Currently, customers whose accounts are set to use Zero Data Retention (ZDR) mus
disable_response_storage = true
```
+### shell_environment_policy
+
+Codex spawns subprocesses (e.g. when executing a `local_shell` tool-call suggested by the assistant). By default it passes **only a minimal core subset** of your environment to those subprocesses to avoid leaking credentials. You can tune this behavior via the **`shell_environment_policy`** block in
+`config.toml`:
+
+```toml
+[shell_environment_policy]
+# inherit can be "core" (default), "all", or "none"
+inherit = "core"
+# set to true to *skip* the filter for `"*KEY*"` and `"*TOKEN*"`
+ignore_default_excludes = false
+# exclude patterns (case-insensitive globs)
+exclude = ["AWS_*", "AZURE_*"]
+# force-set / override values
+set = { CI = "1" }
+# if provided, *only* vars matching these patterns are kept
+include_only = ["PATH", "HOME"]
+```
+
+| Field | Type | Default | Description |
+| ------------------------- | -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| `inherit` | string | `core` | Starting template for the environment:
`core` (`HOME`, `PATH`, `USER`, …), `all` (clone full parent env), or `none` (start empty). |
+| `ignore_default_excludes` | boolean | `false` | When `false`, Codex removes any var whose **name** contains `KEY`, `SECRET`, or `TOKEN` (case-insensitive) before other rules run. |
+| `exclude` | array<string> | `[]` | Case-insensitive glob patterns to drop after the default filter.
Examples: `"AWS_*"`, `"AZURE_*"`. |
+| `set` | table<string,string> | `{}` | Explicit key/value overrides or additions – always win over inherited values. |
+| `include_only` | array<string> | `[]` | If non-empty, a whitelist of patterns; only variables that match _one_ pattern survive the final step. (Generally used with `inherit = "all"`.) |
+
+The patterns are **glob style**, not full regular expressions: `*` matches any
+number of characters, `?` matches exactly one, and character classes like
+`[A-Z]`/`[^0-9]` are supported. Matching is always **case-insensitive**. This
+syntax is documented in code as `EnvironmentVariablePattern` (see
+`core/src/config_types.rs`).
+
+If you just need a clean slate with a few custom entries you can write:
+
+```toml
+[shell_environment_policy]
+inherit = "none"
+set = { PATH = "/usr/bin", MY_FLAG = "1" }
+```
+
+Currently, `CODEX_SANDBOX_NETWORK_DISABLED=1` is also added to the environment, assuming network is disabled. This is not configurable.
+
### notify
Specify a program that will be executed to get notified about events generated by Codex. Note that the program will receive the notification argument as a string of JSON, e.g.:
diff --git a/codex-rs/cli/src/landlock.rs b/codex-rs/cli/src/landlock.rs
index 998072c5ad..5a65fcbca4 100644
--- a/codex-rs/cli/src/landlock.rs
+++ b/codex-rs/cli/src/landlock.rs
@@ -3,27 +3,29 @@
//! On Linux the command is executed inside a Landlock + seccomp sandbox by
//! calling the low-level `exec_linux` helper from `codex_core::linux`.
+use codex_core::config::Config;
use codex_core::exec::StdioPolicy;
use codex_core::exec::spawn_child_sync;
use codex_core::exec_linux::apply_sandbox_policy_to_current_thread;
-use codex_core::protocol::SandboxPolicy;
use std::process::ExitStatus;
use crate::exit_status::handle_exit_status;
/// Execute `command` in a Linux sandbox (Landlock + seccomp) the way Codex
/// would.
-pub fn run_landlock(command: Vec, sandbox_policy: SandboxPolicy) -> anyhow::Result<()> {
+pub fn run_landlock(command: Vec, config: &Config) -> anyhow::Result<()> {
if command.is_empty() {
anyhow::bail!("command args are empty");
}
// Spawn a new thread and apply the sandbox policies there.
+ let env = codex_core::exec_env::create_env(&config.shell_environment_policy);
+ let sandbox_policy = config.sandbox_policy.clone();
let handle = std::thread::spawn(move || -> anyhow::Result {
let cwd = std::env::current_dir()?;
apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?;
- let mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit)?;
+ let mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit, env)?;
let status = child.wait()?;
Ok(status)
});
diff --git a/codex-rs/cli/src/linux-sandbox/main.rs b/codex-rs/cli/src/linux-sandbox/main.rs
index f71f9b863b..3141656595 100644
--- a/codex-rs/cli/src/linux-sandbox/main.rs
+++ b/codex-rs/cli/src/linux-sandbox/main.rs
@@ -10,6 +10,8 @@ fn main() -> anyhow::Result<()> {
use codex_cli::LandlockCommand;
use codex_cli::create_sandbox_policy;
use codex_cli::landlock;
+ use codex_core::config::Config;
+ use codex_core::config::ConfigOverrides;
let LandlockCommand {
full_auto,
@@ -17,6 +19,10 @@ fn main() -> anyhow::Result<()> {
command,
} = LandlockCommand::parse();
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
- landlock::run_landlock(command, sandbox_policy)?;
+ let config = Config::load_with_overrides(ConfigOverrides {
+ sandbox_policy: Some(sandbox_policy),
+ ..Default::default()
+ })?;
+ landlock::run_landlock(command, &config)?;
Ok(())
}
diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs
index aa0691d81e..725a82c255 100644
--- a/codex-rs/cli/src/main.rs
+++ b/codex-rs/cli/src/main.rs
@@ -1,9 +1,13 @@
+use std::path::PathBuf;
+
use clap::Parser;
use codex_cli::LandlockCommand;
use codex_cli::SeatbeltCommand;
use codex_cli::create_sandbox_policy;
use codex_cli::proto;
use codex_cli::seatbelt;
+use codex_core::config::Config;
+use codex_core::config::ConfigOverrides;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
@@ -64,17 +68,23 @@ struct ReplProto {}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
+ let codex_linux_sandbox_exe: Option = if cfg!(target_os = "linux") {
+ std::env::current_exe().ok()
+ } else {
+ None
+ };
+
let cli = MultitoolCli::parse();
match cli.subcommand {
None => {
- codex_tui::run_main(cli.interactive)?;
+ codex_tui::run_main(cli.interactive, codex_linux_sandbox_exe)?;
}
Some(Subcommand::Exec(exec_cli)) => {
- codex_exec::run_main(exec_cli).await?;
+ codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
}
Some(Subcommand::Mcp) => {
- codex_mcp_server::run_main().await?;
+ codex_mcp_server::run_main(codex_linux_sandbox_exe).await?;
}
Some(Subcommand::Proto(proto_cli)) => {
proto::run_main(proto_cli).await?;
@@ -86,7 +96,11 @@ async fn main() -> anyhow::Result<()> {
full_auto,
}) => {
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
- seatbelt::run_seatbelt(command, sandbox_policy).await?;
+ let config = Config::load_with_overrides(ConfigOverrides {
+ sandbox_policy: Some(sandbox_policy),
+ ..Default::default()
+ })?;
+ seatbelt::run_seatbelt(command, &config).await?;
}
#[cfg(unix)]
DebugCommand::Landlock(LandlockCommand {
@@ -95,7 +109,11 @@ async fn main() -> anyhow::Result<()> {
full_auto,
}) => {
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
- codex_cli::landlock::run_landlock(command, sandbox_policy)?;
+ let config = Config::load_with_overrides(ConfigOverrides {
+ sandbox_policy: Some(sandbox_policy),
+ ..Default::default()
+ })?;
+ codex_cli::landlock::run_landlock(command, &config)?;
}
#[cfg(not(unix))]
DebugCommand::Landlock(_) => {
diff --git a/codex-rs/cli/src/seatbelt.rs b/codex-rs/cli/src/seatbelt.rs
index e40848ca0f..d4a7840420 100644
--- a/codex-rs/cli/src/seatbelt.rs
+++ b/codex-rs/cli/src/seatbelt.rs
@@ -1,16 +1,21 @@
+use codex_core::config::Config;
use codex_core::exec::StdioPolicy;
use codex_core::exec::spawn_command_under_seatbelt;
-use codex_core::protocol::SandboxPolicy;
+use codex_core::exec_env::create_env;
use crate::exit_status::handle_exit_status;
-pub async fn run_seatbelt(
- command: Vec,
- sandbox_policy: SandboxPolicy,
-) -> anyhow::Result<()> {
+pub async fn run_seatbelt(command: Vec, config: &Config) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
- let mut child =
- spawn_command_under_seatbelt(command, &sandbox_policy, cwd, StdioPolicy::Inherit).await?;
+ let env = create_env(&config.shell_environment_policy);
+ let mut child = spawn_command_under_seatbelt(
+ command,
+ &config.sandbox_policy,
+ cwd,
+ StdioPolicy::Inherit,
+ env,
+ )
+ .await?;
let status = child.wait().await?;
handle_exit_status(status);
}
diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml
index e2979497d8..2d4ed8f36a 100644
--- a/codex-rs/core/Cargo.toml
+++ b/codex-rs/core/Cargo.toml
@@ -46,6 +46,7 @@ tracing = { version = "0.1.41", features = ["log"] }
tree-sitter = "0.25.3"
tree-sitter-bash = "0.23.3"
uuid = { version = "1", features = ["serde", "v4"] }
+wildmatch = "2.4.0"
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2.172"
@@ -58,6 +59,7 @@ openssl-sys = { version = "*", features = ["vendored"] }
[dev-dependencies]
assert_cmd = "2"
+maplit = "1.0.2"
predicates = "3"
pretty_assertions = "1.4.1"
tempfile = "3"
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 0f91472768..69e504781f 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -37,6 +37,7 @@ use crate::client::ModelClient;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::config::Config;
+use crate::config_types::ShellEnvironmentPolicy;
use crate::conversation_history::ConversationHistory;
use crate::error::CodexErr;
use crate::error::Result as CodexResult;
@@ -45,6 +46,7 @@ use crate::exec::ExecParams;
use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType;
use crate::exec::process_exec_tool_call;
+use crate::exec_env::create_env;
use crate::flags::OPENAI_STREAM_MAX_RETRIES;
use crate::mcp_connection_manager::McpConnectionManager;
use crate::mcp_connection_manager::try_parse_fully_qualified_tool_name;
@@ -171,6 +173,7 @@ pub(crate) struct Session {
instructions: Option,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
+ shell_environment_policy: ShellEnvironmentPolicy,
writable_roots: Mutex>,
/// Manager for external MCP servers/tools.
@@ -634,6 +637,7 @@ async fn submission_loop(
instructions,
approval_policy,
sandbox_policy,
+ shell_environment_policy: config.shell_environment_policy.clone(),
cwd,
writable_roots,
mcp_connection_manager,
@@ -1124,6 +1128,7 @@ fn to_exec_params(params: ShellToolCallParams, sess: &Session) -> ExecParams {
command: params.command,
cwd: sess.resolve_path(params.workdir.clone()),
timeout_ms: params.timeout_ms,
+ env: create_env(&sess.shell_environment_policy),
}
}
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index fd6356ab5f..d643d00660 100644
--- a/codex-rs/core/src/config.rs
+++ b/codex-rs/core/src/config.rs
@@ -1,6 +1,11 @@
use crate::config_profile::ConfigProfile;
+use crate::config_types::History;
+use crate::config_types::McpServerConfig;
+use crate::config_types::ShellEnvironmentPolicy;
+use crate::config_types::ShellEnvironmentPolicyToml;
+use crate::config_types::Tui;
+use crate::config_types::UriBasedFileOpener;
use crate::flags::OPENAI_DEFAULT_MODEL;
-use crate::mcp_server_config::McpServerConfig;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::built_in_model_providers;
use crate::protocol::AskForApproval;
@@ -34,6 +39,8 @@ pub struct Config {
pub sandbox_policy: SandboxPolicy,
+ pub shell_environment_policy: ShellEnvironmentPolicy,
+
/// Disable server-side response storage (sends the full conversation
/// context with every request). Currently necessary for OpenAI customers
/// who have opted into Zero Data Retention (ZDR).
@@ -91,75 +98,14 @@ pub struct Config {
/// Collection of settings that are specific to the TUI.
pub tui: Tui,
-}
-/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
-#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
-pub struct History {
- /// If true, history entries will not be written to disk.
- pub persistence: HistoryPersistence,
-
- /// If set, the maximum size of the history file in bytes.
- /// TODO(mbolin): Not currently honored.
- pub max_bytes: Option,
-}
-
-#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Default)]
-#[serde(rename_all = "kebab-case")]
-pub enum HistoryPersistence {
- /// Save all history entries to disk.
- #[default]
- SaveAll,
- /// Do not write history to disk.
- None,
-}
-
-/// Collection of settings that are specific to the TUI.
-#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
-pub struct Tui {
- /// By default, mouse capture is enabled in the TUI so that it is possible
- /// to scroll the conversation history with a mouse. This comes at the cost
- /// of not being able to use the mouse to select text in the TUI.
- /// (Most terminals support a modifier key to allow this. For example,
- /// text selection works in iTerm if you hold down the `Option` key while
- /// clicking and dragging.)
+ /// Path to the `codex-linux-sandbox` executable. This must be set if
+ /// [`crate::exec::SandboxType::LinuxSeccomp`] is used. Note that this
+ /// cannot be set in the config file: it must be set in code via
+ /// [`ConfigOverrides`].
///
- /// Setting this option to `true` disables mouse capture, so scrolling with
- /// the mouse is not possible, though the keyboard shortcuts e.g. `b` and
- /// `space` still work. This allows the user to select text in the TUI
- /// using the mouse without needing to hold down a modifier key.
- pub disable_mouse_capture: bool,
-}
-
-#[derive(Deserialize, Debug, Copy, Clone, PartialEq)]
-pub enum UriBasedFileOpener {
- #[serde(rename = "vscode")]
- VsCode,
-
- #[serde(rename = "vscode-insiders")]
- VsCodeInsiders,
-
- #[serde(rename = "windsurf")]
- Windsurf,
-
- #[serde(rename = "cursor")]
- Cursor,
-
- /// Option to disable the URI-based file opener.
- #[serde(rename = "none")]
- None,
-}
-
-impl UriBasedFileOpener {
- pub fn get_scheme(&self) -> Option<&str> {
- match self {
- UriBasedFileOpener::VsCode => Some("vscode"),
- UriBasedFileOpener::VsCodeInsiders => Some("vscode-insiders"),
- UriBasedFileOpener::Windsurf => Some("windsurf"),
- UriBasedFileOpener::Cursor => Some("cursor"),
- UriBasedFileOpener::None => None,
- }
- }
+ /// When this program is invoked, arg0 will be set to `codex-linux-sandbox`.
+ pub codex_linux_sandbox_exe: Option,
}
/// Base config deserialized from ~/.codex/config.toml.
@@ -174,6 +120,9 @@ pub struct ConfigToml {
/// Default approval policy for executing commands.
pub approval_policy: Option,
+ #[serde(default)]
+ pub shell_environment_policy: ShellEnvironmentPolicyToml,
+
// The `default` attribute ensures that the field is treated as `None` when
// the key is omitted from the TOML. Without it, Serde treats the field as
// required because we supply a custom deserializer.
@@ -281,6 +230,7 @@ pub struct ConfigOverrides {
pub disable_response_storage: Option,
pub model_provider: Option,
pub config_profile: Option,
+ pub codex_linux_sandbox_exe: Option,
}
impl Config {
@@ -317,6 +267,7 @@ impl Config {
disable_response_storage,
model_provider,
config_profile: config_profile_key,
+ codex_linux_sandbox_exe,
} = overrides;
let config_profile = match config_profile_key.or(cfg.profile) {
@@ -368,6 +319,8 @@ impl Config {
})?
.clone();
+ let shell_environment_policy = cfg.shell_environment_policy.into();
+
let resolved_cwd = {
use std::env;
@@ -402,6 +355,7 @@ impl Config {
.or(cfg.approval_policy)
.unwrap_or_else(AskForApproval::default),
sandbox_policy,
+ shell_environment_policy,
disable_response_storage: disable_response_storage
.or(config_profile.disable_response_storage)
.or(cfg.disable_response_storage)
@@ -415,6 +369,7 @@ impl Config {
history,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
tui: cfg.tui.unwrap_or_default(),
+ codex_linux_sandbox_exe,
};
Ok(config)
}
@@ -523,6 +478,8 @@ pub fn parse_sandbox_permission_with_base_path(
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
+ use crate::config_types::HistoryPersistence;
+
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -741,6 +698,7 @@ disable_response_storage = true
model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
+ shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: false,
instructions: None,
notify: None,
@@ -752,6 +710,7 @@ disable_response_storage = true
history: History::default(),
file_opener: UriBasedFileOpener::VsCode,
tui: Tui::default(),
+ codex_linux_sandbox_exe: None,
},
o3_profile_config
);
@@ -778,6 +737,7 @@ disable_response_storage = true
model_provider: fixture.openai_chat_completions_provider.clone(),
approval_policy: AskForApproval::UnlessAllowListed,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
+ shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: false,
instructions: None,
notify: None,
@@ -789,6 +749,7 @@ disable_response_storage = true
history: History::default(),
file_opener: UriBasedFileOpener::VsCode,
tui: Tui::default(),
+ codex_linux_sandbox_exe: None,
};
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
@@ -830,6 +791,7 @@ disable_response_storage = true
model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::OnFailure,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
+ shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: true,
instructions: None,
notify: None,
@@ -841,6 +803,7 @@ disable_response_storage = true
history: History::default(),
file_opener: UriBasedFileOpener::VsCode,
tui: Tui::default(),
+ codex_linux_sandbox_exe: None,
};
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs
new file mode 100644
index 0000000000..6696f76f0b
--- /dev/null
+++ b/codex-rs/core/src/config_types.rs
@@ -0,0 +1,177 @@
+//! Types used to define the fields of [`crate::config::Config`].
+
+// Note this file should generally be restricted to simple struct/enum
+// definitions that do not contain business logic.
+
+use std::collections::HashMap;
+use wildmatch::WildMatchPattern;
+
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug, Clone, PartialEq)]
+pub struct McpServerConfig {
+ pub command: String,
+
+ #[serde(default)]
+ pub args: Vec,
+
+ #[serde(default)]
+ pub env: Option>,
+}
+
+#[derive(Deserialize, Debug, Copy, Clone, PartialEq)]
+pub enum UriBasedFileOpener {
+ #[serde(rename = "vscode")]
+ VsCode,
+
+ #[serde(rename = "vscode-insiders")]
+ VsCodeInsiders,
+
+ #[serde(rename = "windsurf")]
+ Windsurf,
+
+ #[serde(rename = "cursor")]
+ Cursor,
+
+ /// Option to disable the URI-based file opener.
+ #[serde(rename = "none")]
+ None,
+}
+
+impl UriBasedFileOpener {
+ pub fn get_scheme(&self) -> Option<&str> {
+ match self {
+ UriBasedFileOpener::VsCode => Some("vscode"),
+ UriBasedFileOpener::VsCodeInsiders => Some("vscode-insiders"),
+ UriBasedFileOpener::Windsurf => Some("windsurf"),
+ UriBasedFileOpener::Cursor => Some("cursor"),
+ UriBasedFileOpener::None => None,
+ }
+ }
+}
+
+/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
+#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
+pub struct History {
+ /// If true, history entries will not be written to disk.
+ pub persistence: HistoryPersistence,
+
+ /// If set, the maximum size of the history file in bytes.
+ /// TODO(mbolin): Not currently honored.
+ pub max_bytes: Option,
+}
+
+#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Default)]
+#[serde(rename_all = "kebab-case")]
+pub enum HistoryPersistence {
+ /// Save all history entries to disk.
+ #[default]
+ SaveAll,
+ /// Do not write history to disk.
+ None,
+}
+
+/// Collection of settings that are specific to the TUI.
+#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
+pub struct Tui {
+ /// By default, mouse capture is enabled in the TUI so that it is possible
+ /// to scroll the conversation history with a mouse. This comes at the cost
+ /// of not being able to use the mouse to select text in the TUI.
+ /// (Most terminals support a modifier key to allow this. For example,
+ /// text selection works in iTerm if you hold down the `Option` key while
+ /// clicking and dragging.)
+ ///
+ /// Setting this option to `true` disables mouse capture, so scrolling with
+ /// the mouse is not possible, though the keyboard shortcuts e.g. `b` and
+ /// `space` still work. This allows the user to select text in the TUI
+ /// using the mouse without needing to hold down a modifier key.
+ pub disable_mouse_capture: bool,
+}
+
+#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
+
+pub enum ShellEnvironmentPolicyInherit {
+ /// "Core" environment variables for the platform. On UNIX, this would
+ /// include HOME, LOGNAME, PATH, SHELL, and USER, among others.
+ #[default]
+ Core,
+
+ /// Inherits the full environment from the parent process.
+ All,
+
+ /// Do not inherit any environment variables from the parent process.
+ None,
+}
+
+/// Policy for building the `env` when spawning a process via either the
+/// `shell` or `local_shell` tool.
+#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
+pub struct ShellEnvironmentPolicyToml {
+ pub inherit: Option,
+
+ pub ignore_default_excludes: Option,
+
+ /// List of regular expressions.
+ pub exclude: Option>,
+
+ pub r#set: Option>,
+
+ /// List of regular expressions.
+ pub include_only: Option>,
+}
+
+pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;
+
+/// Deriving the `env` based on this policy works as follows:
+/// 1. Create an initial map based on the `inherit` policy.
+/// 2. If `ignore_default_excludes` is false, filter the map using the default
+/// exclude pattern(s), which are: `"*KEY*"` and `"*TOKEN*"`.
+/// 3. If `exclude` is not empty, filter the map using the provided patterns.
+/// 4. Insert any entries from `r#set` into the map.
+/// 5. If non-empty, filter the map using the `include_only` patterns.
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct ShellEnvironmentPolicy {
+ /// Starting point when building the environment.
+ pub inherit: ShellEnvironmentPolicyInherit,
+
+ /// True to skip the check to exclude default environment variables that
+ /// contain "KEY" or "TOKEN" in their name.
+ pub ignore_default_excludes: bool,
+
+ /// Environment variable names to exclude from the environment.
+ pub exclude: Vec,
+
+ /// (key, value) pairs to insert in the environment.
+ pub r#set: HashMap,
+
+ /// Environment variable names to retain in the environment.
+ pub include_only: Vec,
+}
+
+impl From for ShellEnvironmentPolicy {
+ fn from(toml: ShellEnvironmentPolicyToml) -> Self {
+ let inherit = toml.inherit.unwrap_or(ShellEnvironmentPolicyInherit::Core);
+ let ignore_default_excludes = toml.ignore_default_excludes.unwrap_or(false);
+ let exclude = toml
+ .exclude
+ .unwrap_or_default()
+ .into_iter()
+ .map(|s| EnvironmentVariablePattern::new_case_insensitive(&s))
+ .collect();
+ let r#set = toml.r#set.unwrap_or_default();
+ let include_only = toml
+ .include_only
+ .unwrap_or_default()
+ .into_iter()
+ .map(|s| EnvironmentVariablePattern::new_case_insensitive(&s))
+ .collect();
+
+ Self {
+ inherit,
+ ignore_default_excludes,
+ exclude,
+ r#set,
+ include_only,
+ }
+ }
+}
diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs
index 158a0da9b4..96b601b613 100644
--- a/codex-rs/core/src/exec.rs
+++ b/codex-rs/core/src/exec.rs
@@ -1,6 +1,7 @@
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
+use std::collections::HashMap;
use std::io;
use std::path::Path;
use std::path::PathBuf;
@@ -59,6 +60,7 @@ pub struct ExecParams {
pub command: Vec,
pub cwd: PathBuf,
pub timeout_ms: Option,
+ pub env: HashMap,
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -87,12 +89,14 @@ pub async fn process_exec_tool_call(
command,
cwd,
timeout_ms,
+ env,
} = params;
let child = spawn_command_under_seatbelt(
command,
sandbox_policy,
cwd,
StdioPolicy::RedirectForShellTool,
+ env,
)
.await?;
consume_truncated_output(child, ctrl_c, timeout_ms).await
@@ -145,9 +149,10 @@ pub async fn spawn_command_under_seatbelt(
sandbox_policy: &SandboxPolicy,
cwd: PathBuf,
stdio_policy: StdioPolicy,
+ env: HashMap,
) -> std::io::Result {
let seatbelt_command = create_seatbelt_command(command, sandbox_policy, &cwd);
- spawn_child_async(seatbelt_command, cwd, sandbox_policy, stdio_policy).await
+ spawn_child_async(seatbelt_command, cwd, sandbox_policy, stdio_policy, env).await
}
fn create_seatbelt_command(
@@ -233,6 +238,7 @@ async fn exec(
command,
cwd,
timeout_ms,
+ env,
}: ExecParams,
sandbox_policy: &SandboxPolicy,
ctrl_c: Arc,
@@ -242,6 +248,7 @@ async fn exec(
cwd,
sandbox_policy,
StdioPolicy::RedirectForShellTool,
+ env,
)
.await?;
consume_truncated_output(child, ctrl_c, timeout_ms).await
@@ -259,7 +266,8 @@ macro_rules! configure_command {
$command: expr,
$cwd: expr,
$sandbox_policy: expr,
- $stdio_policy: expr
+ $stdio_policy: expr,
+ $env_map: expr
) => {{
// For now, we take `SandboxPolicy` as a parameter to spawn_child() because
// we need to determine whether to set the
@@ -279,6 +287,38 @@ macro_rules! configure_command {
cmd.args(&$command[1..]);
cmd.current_dir($cwd);
+ // Previously, to update the env for `cmd`, we did the straightforward
+ // thing of calling `env_clear()` followed by `envs(&env_map)` so
+ // that the spawned process inherited *only* the variables explicitly
+ // provided by the caller. On Linux, the combination of `env_clear()`
+ // and Landlock/seccomp caused a permission error whereas this more
+ // "surgical" approach of setting variables individually appears to
+ // work fine. More time with `strace` and friends is merited to fully
+ // debug thus, though we will soon use a helper binary like we do for
+ // Seatbelt, which will simplify this logic.
+
+ // Iterate through the current process environment first so we can
+ // decide, for every variable that already exists, whether we need to
+ // override its value.
+ let mut remaining_overrides = $env_map.clone();
+ for (key, current_val) in std::env::vars() {
+ if let Some(desired_val) = remaining_overrides.remove(&key) {
+ // The caller provided a value for this variable. Override it
+ // only if the value differs from what is currently set.
+ if desired_val != current_val {
+ cmd.env(&key, desired_val);
+ }
+ }
+ // If the variable was not in `env_map`, we leave it unchanged.
+ }
+
+ // Any entries still left in `remaining_overrides` were not present in
+ // the parent environment. Add them now so that the child process sees
+ // the complete set requested by the caller.
+ for (key, val) in remaining_overrides {
+ cmd.env(key, val);
+ }
+
if !$sandbox_policy.has_full_network_access() {
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
}
@@ -313,8 +353,9 @@ pub(crate) async fn spawn_child_async(
cwd: PathBuf,
sandbox_policy: &SandboxPolicy,
stdio_policy: StdioPolicy,
+ env: HashMap,
) -> std::io::Result {
- let mut cmd = configure_command!(Command, command, cwd, sandbox_policy, stdio_policy)?;
+ let mut cmd = configure_command!(Command, command, cwd, sandbox_policy, stdio_policy, env)?;
cmd.kill_on_drop(true).spawn()
}
@@ -326,13 +367,15 @@ pub fn spawn_child_sync(
cwd: PathBuf,
sandbox_policy: &SandboxPolicy,
stdio_policy: StdioPolicy,
+ env: HashMap,
) -> std::io::Result {
let mut cmd = configure_command!(
std::process::Command,
command,
cwd,
sandbox_policy,
- stdio_policy
+ stdio_policy,
+ env
)?;
cmd.spawn()
}
diff --git a/codex-rs/core/src/exec_env.rs b/codex-rs/core/src/exec_env.rs
new file mode 100644
index 0000000000..2957f3da15
--- /dev/null
+++ b/codex-rs/core/src/exec_env.rs
@@ -0,0 +1,196 @@
+use crate::config_types::EnvironmentVariablePattern;
+use crate::config_types::ShellEnvironmentPolicy;
+use crate::config_types::ShellEnvironmentPolicyInherit;
+use std::collections::HashMap;
+use std::collections::HashSet;
+
+/// Construct an environment map based on the rules in the specified policy. The
+/// resulting map can be passed directly to `Command::envs()` after calling
+/// `env_clear()` to ensure no unintended variables are leaked to the spawned
+/// process.
+///
+/// The derivation follows the algorithm documented in the struct-level comment
+/// for [`ShellEnvironmentPolicy`].
+pub fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap {
+ populate_env(std::env::vars(), policy)
+}
+
+fn populate_env(vars: I, policy: &ShellEnvironmentPolicy) -> HashMap
+where
+ I: IntoIterator- ,
+{
+ // Step 1 – determine the starting set of variables based on the
+ // `inherit` strategy.
+ let mut env_map: HashMap = match policy.inherit {
+ ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
+ ShellEnvironmentPolicyInherit::None => HashMap::new(),
+ ShellEnvironmentPolicyInherit::Core => {
+ const CORE_VARS: &[&str] = &[
+ "HOME", "LOGNAME", "PATH", "SHELL", "USER", "USERNAME", "TMPDIR", "TEMP", "TMP",
+ ];
+ let allow: HashSet<&str> = CORE_VARS.iter().copied().collect();
+ vars.into_iter()
+ .filter(|(k, _)| allow.contains(k.as_str()))
+ .collect()
+ }
+ };
+
+ // Internal helper – does `name` match **any** pattern in `patterns`?
+ let matches_any = |name: &str, patterns: &[EnvironmentVariablePattern]| -> bool {
+ patterns.iter().any(|pattern| pattern.matches(name))
+ };
+
+ // Step 2 – Apply the default exclude if not disabled.
+ if !policy.ignore_default_excludes {
+ let default_excludes = vec![
+ EnvironmentVariablePattern::new_case_insensitive("*KEY*"),
+ EnvironmentVariablePattern::new_case_insensitive("*SECRET*"),
+ EnvironmentVariablePattern::new_case_insensitive("*TOKEN*"),
+ ];
+ env_map.retain(|k, _| !matches_any(k, &default_excludes));
+ }
+
+ // Step 3 – Apply custom excludes.
+ if !policy.exclude.is_empty() {
+ env_map.retain(|k, _| !matches_any(k, &policy.exclude));
+ }
+
+ // Step 4 – Apply user-provided overrides.
+ for (key, val) in &policy.r#set {
+ env_map.insert(key.clone(), val.clone());
+ }
+
+ // Step 5 – If include_only is non-empty, keep *only* the matching vars.
+ if !policy.include_only.is_empty() {
+ env_map.retain(|k, _| matches_any(k, &policy.include_only));
+ }
+
+ env_map
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used, clippy::expect_used)]
+
+ use super::*;
+ use crate::config_types::ShellEnvironmentPolicyInherit;
+ use maplit::hashmap;
+
+ fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
+ pairs
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect()
+ }
+
+ #[test]
+ fn test_core_inherit_and_default_excludes() {
+ let vars = make_vars(&[
+ ("PATH", "/usr/bin"),
+ ("HOME", "/home/user"),
+ ("API_KEY", "secret"),
+ ("SECRET_TOKEN", "t"),
+ ]);
+
+ let policy = ShellEnvironmentPolicy::default(); // inherit Core, default excludes on
+ let result = populate_env(vars, &policy);
+
+ let expected: HashMap = hashmap! {
+ "PATH".to_string() => "/usr/bin".to_string(),
+ "HOME".to_string() => "/home/user".to_string(),
+ };
+
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_include_only() {
+ let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
+
+ let policy = ShellEnvironmentPolicy {
+ // skip default excludes so nothing is removed prematurely
+ ignore_default_excludes: true,
+ include_only: vec![EnvironmentVariablePattern::new_case_insensitive("*PATH")],
+ ..Default::default()
+ };
+
+ let result = populate_env(vars, &policy);
+
+ let expected: HashMap = hashmap! {
+ "PATH".to_string() => "/usr/bin".to_string(),
+ };
+
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_set_overrides() {
+ let vars = make_vars(&[("PATH", "/usr/bin")]);
+
+ let mut policy = ShellEnvironmentPolicy {
+ ignore_default_excludes: true,
+ ..Default::default()
+ };
+ policy.r#set.insert("NEW_VAR".to_string(), "42".to_string());
+
+ let result = populate_env(vars, &policy);
+
+ let expected: HashMap = hashmap! {
+ "PATH".to_string() => "/usr/bin".to_string(),
+ "NEW_VAR".to_string() => "42".to_string(),
+ };
+
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_inherit_all() {
+ let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
+
+ let policy = ShellEnvironmentPolicy {
+ inherit: ShellEnvironmentPolicyInherit::All,
+ ignore_default_excludes: true, // keep everything
+ ..Default::default()
+ };
+
+ let result = populate_env(vars.clone(), &policy);
+ let expected: HashMap = vars.into_iter().collect();
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_inherit_all_with_default_excludes() {
+ let vars = make_vars(&[("PATH", "/usr/bin"), ("API_KEY", "secret")]);
+
+ let policy = ShellEnvironmentPolicy {
+ inherit: ShellEnvironmentPolicyInherit::All,
+ ..Default::default()
+ };
+
+ let result = populate_env(vars, &policy);
+ let expected: HashMap = hashmap! {
+ "PATH".to_string() => "/usr/bin".to_string(),
+ };
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_inherit_none() {
+ let vars = make_vars(&[("PATH", "/usr/bin"), ("HOME", "/home")]);
+
+ let mut policy = ShellEnvironmentPolicy {
+ inherit: ShellEnvironmentPolicyInherit::None,
+ ignore_default_excludes: true,
+ ..Default::default()
+ };
+ policy
+ .r#set
+ .insert("ONLY_VAR".to_string(), "yes".to_string());
+
+ let result = populate_env(vars, &policy);
+ let expected: HashMap = hashmap! {
+ "ONLY_VAR".to_string() => "yes".to_string(),
+ };
+ assert_eq!(result, expected);
+ }
+}
diff --git a/codex-rs/core/src/exec_linux.rs b/codex-rs/core/src/exec_linux.rs
index e74c56219c..76bd428a7f 100644
--- a/codex-rs/core/src/exec_linux.rs
+++ b/codex-rs/core/src/exec_linux.rs
@@ -34,6 +34,7 @@ pub fn exec_linux(
command,
cwd,
timeout_ms,
+ env,
} = params;
apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?;
let child = spawn_child_async(
@@ -41,6 +42,7 @@ pub fn exec_linux(
cwd,
&sandbox_policy,
StdioPolicy::RedirectForShellTool,
+ env,
)
.await?;
consume_truncated_output(child, ctrl_c_copy, timeout_ms).await
diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs
index 6e9b8de7c6..07c568151a 100644
--- a/codex-rs/core/src/landlock.rs
+++ b/codex-rs/core/src/landlock.rs
@@ -143,20 +143,29 @@ mod tests {
#![expect(clippy::unwrap_used, clippy::expect_used)]
use super::*;
+ use crate::config_types::ShellEnvironmentPolicy;
use crate::exec::ExecParams;
use crate::exec::SandboxType;
use crate::exec::process_exec_tool_call;
+ use crate::exec_env::create_env;
use crate::protocol::SandboxPolicy;
+ use std::collections::HashMap;
use std::sync::Arc;
use tempfile::NamedTempFile;
use tokio::sync::Notify;
+ fn create_env_from_core_vars() -> HashMap {
+ let policy = ShellEnvironmentPolicy::default();
+ create_env(&policy)
+ }
+
#[allow(clippy::print_stdout)]
async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) {
let params = ExecParams {
command: cmd.iter().map(|elm| elm.to_string()).collect(),
cwd: std::env::current_dir().expect("cwd should exist"),
timeout_ms: Some(timeout_ms),
+ env: create_env_from_core_vars(),
};
let sandbox_policy =
@@ -236,9 +245,10 @@ mod tests {
let params = ExecParams {
command: cmd.iter().map(|s| s.to_string()).collect(),
cwd: std::env::current_dir().expect("cwd should exist"),
- // Give the tool a generous 2‑second timeout so even slow DNS timeouts
+ // Give the tool a generous 2-second timeout so even slow DNS timeouts
// do not stall the suite.
timeout_ms: Some(2_000),
+ env: create_env_from_core_vars(),
};
let sandbox_policy = SandboxPolicy::new_read_only_policy();
diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs
index 00a65a6725..261ae0a0fd 100644
--- a/codex-rs/core/src/lib.rs
+++ b/codex-rs/core/src/lib.rs
@@ -13,16 +13,17 @@ pub use codex::Codex;
pub mod codex_wrapper;
pub mod config;
pub mod config_profile;
+pub mod config_types;
mod conversation_history;
pub mod error;
pub mod exec;
+pub mod exec_env;
pub mod exec_linux;
mod flags;
mod is_safe_command;
#[cfg(target_os = "linux")]
pub mod landlock;
mod mcp_connection_manager;
-pub mod mcp_server_config;
mod mcp_tool_call;
mod message_history;
mod model_provider_info;
diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs
index 714c9452ff..6ae1865f16 100644
--- a/codex-rs/core/src/mcp_connection_manager.rs
+++ b/codex-rs/core/src/mcp_connection_manager.rs
@@ -19,7 +19,7 @@ use mcp_types::Tool;
use tokio::task::JoinSet;
use tracing::info;
-use crate::mcp_server_config::McpServerConfig;
+use crate::config_types::McpServerConfig;
/// Delimiter used to separate the server name from the tool name in a fully
/// qualified tool name.
diff --git a/codex-rs/core/src/mcp_server_config.rs b/codex-rs/core/src/mcp_server_config.rs
deleted file mode 100644
index 30845431fa..0000000000
--- a/codex-rs/core/src/mcp_server_config.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use std::collections::HashMap;
-
-use serde::Deserialize;
-
-#[derive(Deserialize, Debug, Clone, PartialEq)]
-pub struct McpServerConfig {
- pub command: String,
-
- #[serde(default)]
- pub args: Vec,
-
- #[serde(default)]
- pub env: Option>,
-}
diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs
index 6c201dfd43..29970c2a74 100644
--- a/codex-rs/core/src/message_history.rs
+++ b/codex-rs/core/src/message_history.rs
@@ -28,7 +28,7 @@ use tokio::io::AsyncReadExt;
use uuid::Uuid;
use crate::config::Config;
-use crate::config::HistoryPersistence;
+use crate::config_types::HistoryPersistence;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs
index 65f2204dae..676b47d64f 100644
--- a/codex-rs/exec/src/event_processor.rs
+++ b/codex-rs/exec/src/event_processor.rs
@@ -1,5 +1,6 @@
use chrono::Utc;
use codex_common::elapsed::format_elapsed;
+use codex_core::config::Config;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::BackgroundEventEvent;
use codex_core::protocol::ErrorEvent;
@@ -13,7 +14,6 @@ use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::PatchApplyEndEvent;
use codex_core::protocol::SessionConfiguredEvent;
-use codex_core::protocol::TaskCompleteEvent;
use owo_colors::OwoColorize;
use owo_colors::Style;
use shlex::try_join;
@@ -103,9 +103,36 @@ macro_rules! ts_println {
}};
}
+/// Print a concise summary of the effective configuration that will be used
+/// for the session. This mirrors the information shown in the TUI welcome
+/// screen.
+pub(crate) fn print_config_summary(config: &Config, with_ansi: bool) {
+ let bold = if with_ansi {
+ Style::new().bold()
+ } else {
+ Style::new()
+ };
+
+ ts_println!("OpenAI Codex (research preview)\n--------");
+
+ let entries = vec![
+ ("workdir", config.cwd.display().to_string()),
+ ("model", config.model.clone()),
+ ("provider", config.model_provider_id.clone()),
+ ("approval", format!("{:?}", config.approval_policy)),
+ ("sandbox", format!("{:?}", config.sandbox_policy)),
+ ];
+
+ for (key, value) in entries {
+ println!("{} {}", format!("{key}: ").style(bold), value);
+ }
+
+ println!("--------\n");
+}
+
impl EventProcessor {
pub(crate) fn process_event(&mut self, event: Event) {
- let Event { id, msg } = event;
+ let Event { id: _, msg } = event;
match msg {
EventMsg::Error(ErrorEvent { message }) => {
let prefix = "ERROR:".style(self.red);
@@ -114,15 +141,8 @@ impl EventProcessor {
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
ts_println!("{}", message.style(self.dimmed));
}
- EventMsg::TaskStarted => {
- let msg = format!("Task started: {id}");
- ts_println!("{}", msg.style(self.dimmed));
- }
- EventMsg::TaskComplete(TaskCompleteEvent {
- last_agent_message: _,
- }) => {
- let msg = format!("Task complete: {id}");
- ts_println!("{}", msg.style(self.bold));
+ EventMsg::TaskStarted | EventMsg::TaskComplete(_) => {
+ // Ignore.
}
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
let prefix = "Agent message:".style(self.bold);
@@ -385,7 +405,15 @@ impl EventProcessor {
history_log_id: _,
history_entry_count: _,
} = session_configured_event;
- println!("session {session_id} with model {model}");
+
+ ts_println!(
+ "{} {}",
+ "codex session".style(self.magenta).style(self.bold),
+ session_id.to_string().style(self.dimmed)
+ );
+
+ ts_println!("model: {}", model);
+ println!();
}
EventMsg::GetHistoryEntryResponse(_) => {
// Currently ignored in exec output.
diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs
index 242983ea2e..d4a884b3a1 100644
--- a/codex-rs/exec/src/lib.rs
+++ b/codex-rs/exec/src/lib.rs
@@ -3,6 +3,7 @@ mod event_processor;
use std::io::IsTerminal;
use std::path::Path;
+use std::path::PathBuf;
use std::sync::Arc;
pub use cli::Cli;
@@ -18,13 +19,14 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::util::is_inside_git_repo;
use event_processor::EventProcessor;
+use event_processor::print_config_summary;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing_subscriber::EnvFilter;
use std::io::Write;
-pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
+pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> {
let Cli {
images,
model,
@@ -70,8 +72,11 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
},
cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)),
model_provider: None,
+ codex_linux_sandbox_exe,
};
let config = Config::load_with_overrides(overrides)?;
+ // Print the effective configuration so users can see what Codex is using.
+ print_config_summary(&config, stdout_with_ansi);
if !skip_git_repo_check && !is_inside_git_repo(&config) {
eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");
diff --git a/codex-rs/exec/src/main.rs b/codex-rs/exec/src/main.rs
index 3a40da2336..3cb7bd0b66 100644
--- a/codex-rs/exec/src/main.rs
+++ b/codex-rs/exec/src/main.rs
@@ -1,11 +1,19 @@
+use std::path::PathBuf;
+
use clap::Parser;
use codex_exec::Cli;
use codex_exec::run_main;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
+ let codex_linux_sandbox_exe: Option = if cfg!(target_os = "linux") {
+ std::env::current_exe().ok()
+ } else {
+ None
+ };
+
let cli = Cli::parse();
- run_main(cli).await?;
+ run_main(cli, codex_linux_sandbox_exe).await?;
Ok(())
}
diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs
index 2ddc00fbf9..d04a5c80bc 100644
--- a/codex-rs/mcp-server/src/codex_tool_config.rs
+++ b/codex-rs/mcp-server/src/codex_tool_config.rs
@@ -144,7 +144,10 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
impl CodexToolCallParam {
/// Returns the initial user prompt to start the Codex conversation and the
/// Config.
- pub fn into_config(self) -> std::io::Result<(String, codex_core::config::Config)> {
+ pub fn into_config(
+ self,
+ codex_linux_sandbox_exe: Option,
+ ) -> std::io::Result<(String, codex_core::config::Config)> {
let Self {
prompt,
model,
@@ -167,6 +170,7 @@ impl CodexToolCallParam {
sandbox_policy,
disable_response_storage,
model_provider: None,
+ codex_linux_sandbox_exe,
};
let cfg = codex_core::config::Config::load_with_overrides(overrides)?;
diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs
index e621f779f0..0f29eb7826 100644
--- a/codex-rs/mcp-server/src/lib.rs
+++ b/codex-rs/mcp-server/src/lib.rs
@@ -2,6 +2,7 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]
use std::io::Result as IoResult;
+use std::path::PathBuf;
use mcp_types::JSONRPCMessage;
use tokio::io::AsyncBufReadExt;
@@ -24,7 +25,7 @@ use crate::message_processor::MessageProcessor;
/// plenty for an interactive CLI.
const CHANNEL_CAPACITY: usize = 128;
-pub async fn run_main() -> IoResult<()> {
+pub async fn run_main(codex_linux_sandbox_exe: Option) -> IoResult<()> {
// Install a simple subscriber so `tracing` output is visible. Users can
// control the log level with `RUST_LOG`.
tracing_subscriber::fmt()
@@ -61,7 +62,7 @@ pub async fn run_main() -> IoResult<()> {
// Task: process incoming messages.
let processor_handle = tokio::spawn({
- let mut processor = MessageProcessor::new(outgoing_tx.clone());
+ let mut processor = MessageProcessor::new(outgoing_tx.clone(), codex_linux_sandbox_exe);
async move {
while let Some(msg) = incoming_rx.recv().await {
match msg {
diff --git a/codex-rs/mcp-server/src/main.rs b/codex-rs/mcp-server/src/main.rs
index baef8587f7..8ce727e920 100644
--- a/codex-rs/mcp-server/src/main.rs
+++ b/codex-rs/mcp-server/src/main.rs
@@ -1,7 +1,15 @@
+use std::path::PathBuf;
+
use codex_mcp_server::run_main;
#[tokio::main]
async fn main() -> std::io::Result<()> {
- run_main().await?;
+ let codex_linux_sandbox_exe: Option = if cfg!(target_os = "linux") {
+ std::env::current_exe().ok()
+ } else {
+ None
+ };
+
+ run_main(codex_linux_sandbox_exe).await?;
Ok(())
}
diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs
index 299523f9c5..bf6f42e569 100644
--- a/codex-rs/mcp-server/src/message_processor.rs
+++ b/codex-rs/mcp-server/src/message_processor.rs
@@ -1,3 +1,5 @@
+use std::path::PathBuf;
+
use crate::codex_tool_config::CodexToolCallParam;
use crate::codex_tool_config::create_tool_for_codex_tool_call_param;
@@ -28,15 +30,20 @@ use tokio::task;
pub(crate) struct MessageProcessor {
outgoing: mpsc::Sender,
initialized: bool,
+ codex_linux_sandbox_exe: Option,
}
impl MessageProcessor {
/// Create a new `MessageProcessor`, retaining a handle to the outgoing
/// `Sender` so handlers can enqueue messages to be written to stdout.
- pub(crate) fn new(outgoing: mpsc::Sender) -> Self {
+ pub(crate) fn new(
+ outgoing: mpsc::Sender,
+ codex_linux_sandbox_exe: Option,
+ ) -> Self {
Self {
outgoing,
initialized: false,
+ codex_linux_sandbox_exe,
}
}
@@ -339,7 +346,7 @@ impl MessageProcessor {
let (initial_prompt, config): (String, CodexConfig) = match arguments {
Some(json_val) => match serde_json::from_value::(json_val) {
- Ok(tool_cfg) => match tool_cfg.into_config() {
+ Ok(tool_cfg) => match tool_cfg.into_config(self.codex_linux_sandbox_exe.clone()) {
Ok(cfg) => cfg,
Err(e) => {
let result = CallToolResult {
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
index f4391785f8..4ab68724aa 100644
--- a/codex-rs/tui/src/lib.rs
+++ b/codex-rs/tui/src/lib.rs
@@ -10,6 +10,7 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::util::is_inside_git_repo;
use log_layer::TuiLogLayer;
use std::fs::OpenOptions;
+use std::path::PathBuf;
use tracing_appender::non_blocking;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
@@ -36,7 +37,7 @@ mod user_approval_widget;
pub use cli::Cli;
-pub fn run_main(cli: Cli) -> std::io::Result<()> {
+pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> std::io::Result<()> {
let (sandbox_policy, approval_policy) = if cli.full_auto {
(
Some(SandboxPolicy::new_full_auto_policy()),
@@ -61,6 +62,7 @@ pub fn run_main(cli: Cli) -> std::io::Result<()> {
cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)),
model_provider: None,
config_profile: cli.config_profile.clone(),
+ codex_linux_sandbox_exe,
};
#[allow(clippy::print_stderr)]
match Config::load_with_overrides(overrides) {
diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs
index 531682daae..08738ba245 100644
--- a/codex-rs/tui/src/main.rs
+++ b/codex-rs/tui/src/main.rs
@@ -1,10 +1,18 @@
+use std::path::PathBuf;
+
use clap::Parser;
use codex_tui::Cli;
use codex_tui::run_main;
#[tokio::main]
async fn main() -> std::io::Result<()> {
+ let codex_linux_sandbox_exe: Option = if cfg!(target_os = "linux") {
+ std::env::current_exe().ok()
+ } else {
+ None
+ };
+
let cli = Cli::parse();
- run_main(cli)?;
+ run_main(cli, codex_linux_sandbox_exe)?;
Ok(())
}
diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs
index 118eaa59b6..a56ce7749e 100644
--- a/codex-rs/tui/src/markdown.rs
+++ b/codex-rs/tui/src/markdown.rs
@@ -1,5 +1,5 @@
use codex_core::config::Config;
-use codex_core::config::UriBasedFileOpener;
+use codex_core::config_types::UriBasedFileOpener;
use ratatui::text::Line;
use ratatui::text::Span;
use std::borrow::Cow;