Files
codex/prs/bolinfest/PR-1653.md
2025-09-02 15:17:45 -07:00

4.9 KiB
Raw Blame History

PR #1653: feat: support dotenv (including ~/.codex/.env)

Description

This PR adds a load_dotenv() helper function to the codex-common crate that is available when the cli feature is enabled. The function uses dotenvy to update the environment from:

  • $CODEX_HOME/.env
  • $(pwd)/.env

To test:

  • ran printenv OPENAI_API_KEY to verify the env var exists in my environment
  • ran just codex exec hello to verify the CLI uses my OPENAI_API_KEY
  • ran unset OPENAI_API_KEY
  • ran just codex exec hello again and got ERROR: Missing environment variable: OPENAI_API_KEY, as expected
  • created ~/.codex/.env and added OPENAI_API_KEY=sk-proj-... (also ran chmod 400 ~/.codex/.env for good measure)
  • ran just codex exec hello again and it worked, verifying it picked up OPENAI_API_KEY from ~/.codex/.env

Note this functionality was available in the TypeScript CLI: https://github.com/openai/codex/pull/122 and was recently requested over on https://github.com/openai/codex/issues/1262#issuecomment-3093203551.

Full Diff

diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 9b4a4e32d4..3e4b84a435 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -756,7 +756,9 @@ version = "0.0.0"
 dependencies = [
  "anyhow",
  "clap",
+ "codex-common",
  "codex-core",
+ "dotenvy",
  "landlock",
  "libc",
  "seccompiler",
@@ -1272,6 +1274,12 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
 [[package]]
 name = "dupe"
 version = "0.9.1"
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index 8ed06c45af..2dfd3e55fe 100644
--- a/codex-rs/core/src/config.rs
+++ b/codex-rs/core/src/config.rs
@@ -561,7 +561,7 @@ fn default_model() -> String {
 ///   function will Err if the path does not exist.
 /// - If `CODEX_HOME` is not set, this function does not verify that the
 ///   directory exists.
-fn find_codex_home() -> std::io::Result<PathBuf> {
+pub fn find_codex_home() -> std::io::Result<PathBuf> {
     // Honor the `CODEX_HOME` environment variable when it is set to allow users
     // (and tests) to override the default location.
     if let Ok(val) = std::env::var("CODEX_HOME") {
diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml
index c8cd1078c0..5c2dea6083 100644
--- a/codex-rs/linux-sandbox/Cargo.toml
+++ b/codex-rs/linux-sandbox/Cargo.toml
@@ -17,7 +17,9 @@ workspace = true
 [dependencies]
 anyhow = "1"
 clap = { version = "4", features = ["derive"] }
+codex-common = { path = "../common", features = ["cli"] }
 codex-core = { path = "../core" }
+dotenvy = "0.15.7"
 tokio = { version = "1", features = ["rt-multi-thread"] }
 
 [dev-dependencies]
diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/linux-sandbox/src/lib.rs
index 568f015822..960678467c 100644
--- a/codex-rs/linux-sandbox/src/lib.rs
+++ b/codex-rs/linux-sandbox/src/lib.rs
@@ -43,6 +43,10 @@ where
         crate::run_main();
     }
 
+    // This modifies the environment, which is not thread-safe, so do this
+    // before creating any threads/the Tokio runtime.
+    load_dotenv();
+
     // Regular invocation  create a Tokio runtime and execute the provided
     // async entry-point.
     let runtime = tokio::runtime::Runtime::new()?;
@@ -61,3 +65,11 @@ where
 pub fn run_main() -> ! {
     panic!("codex-linux-sandbox is only supported on Linux");
 }
+
+/// Load env vars from ~/.codex/.env and `$(pwd)/.env`.
+fn load_dotenv() {
+    if let Ok(codex_home) = codex_core::config::find_codex_home() {
+        dotenvy::from_path(codex_home.join(".env")).ok();
+    }
+    dotenvy::dotenv().ok();
+}

Review Comments

codex-rs/common/src/dotenv.rs

@@ -0,0 +1,7 @@
+/// Load env vars from ~/.codex/.env and `$(pwd)/.env`.
+pub fn load_dotenv() {
+    if let Ok(codex_home) = codex_core::config::find_codex_home() {
+        dotenvy::from_path(codex_home.join(".env")).ok();
+    }
+    dotenvy::dotenv().ok();

I think this could be argued either way, but I think there is a technical reason to do this as early as possible (before we know what the Config even is), which is that setting environment variables for the current process is not thread-safe, so it should really be done before any threads have been created.

I just reworked this so that load_dotenv() is now called before we set up the Tokio runtime.