Skip loading codex home as project layer (#10207)

Summary:
- Fixes issue #9932: https://github.com/openai/codex/issues/9932
- Prevents `$CODEX_HOME` (typically `~/.codex`) from being discovered as
a project `.codex` layer by skipping it during project layer traversal.
We compare both normalized absolute paths and best-effort canonicalized
paths to handle symlinks.
- Adds regression tests for home-directory invocation and for the case
where `CODEX_HOME` points to a project `.codex` directory (e.g.,
worktrees/editor integrations).

Testing:
- `cargo build -p codex-cli --bin codex`
- `cargo build -p codex-rmcp-client --bin test_stdio_server`
- `cargo test -p codex-core`
- `cargo test --all-features`
- Manual: ran `target/debug/codex` from `~` and confirmed the
disabled-folder warning and trust prompt no longer appear.
This commit is contained in:
daniel-oai
2026-01-30 12:42:07 -08:00
committed by GitHub
parent 83317ed4bf
commit dd6c1d3787
2 changed files with 106 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ use codex_protocol::config_types::TrustLevel;
use codex_protocol::protocol::AskForApproval;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use dunce::canonicalize as normalize_path;
use serde::Deserialize;
use std::io;
use std::path::Path;
@@ -236,6 +237,7 @@ pub async fn load_config_layers_state(
&cwd,
&project_trust_context.project_root,
&project_trust_context,
codex_home,
)
.await?;
layers.extend(project_layers);
@@ -671,7 +673,11 @@ async fn load_project_layers(
cwd: &AbsolutePathBuf,
project_root: &AbsolutePathBuf,
trust_context: &ProjectTrustContext,
codex_home: &Path,
) -> io::Result<Vec<ConfigLayerEntry>> {
let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home)?;
let codex_home_normalized =
normalize_path(codex_home_abs.as_path()).unwrap_or_else(|_| codex_home_abs.to_path_buf());
let mut dirs = cwd
.as_path()
.ancestors()
@@ -702,6 +708,11 @@ async fn load_project_layers(
let layer_dir = AbsolutePathBuf::from_absolute_path(dir)?;
let decision = trust_context.decision_for_dir(&layer_dir);
let dot_codex_abs = AbsolutePathBuf::from_absolute_path(&dot_codex)?;
let dot_codex_normalized =
normalize_path(dot_codex_abs.as_path()).unwrap_or_else(|_| dot_codex_abs.to_path_buf());
if dot_codex_abs == codex_home_abs || dot_codex_normalized == codex_home_normalized {
continue;
}
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE)?;
match tokio::fs::read_to_string(&config_file).await {
Ok(contents) => {

View File

@@ -755,6 +755,101 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
Ok(())
}
#[tokio::test]
async fn codex_home_is_not_loaded_as_project_layer_from_home_dir() -> std::io::Result<()> {
let tmp = tempdir()?;
let home_dir = tmp.path().join("home");
let codex_home = home_dir.join(".codex");
tokio::fs::create_dir_all(&codex_home).await?;
tokio::fs::write(codex_home.join(CONFIG_TOML_FILE), "foo = \"user\"\n").await?;
let cwd = AbsolutePathBuf::from_absolute_path(&home_dir)?;
let layers = load_config_layers_state(
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
)
.await?;
let project_layers: Vec<_> = layers
.get_layers(
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
true,
)
.into_iter()
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
.collect();
let expected: Vec<&ConfigLayerEntry> = Vec::new();
assert_eq!(expected, project_layers);
assert_eq!(
layers.effective_config().get("foo"),
Some(&TomlValue::String("user".to_string()))
);
Ok(())
}
#[tokio::test]
async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Result<()> {
let tmp = tempdir()?;
let project_root = tmp.path().join("project");
let nested = project_root.join("child");
let project_dot_codex = project_root.join(".codex");
let nested_dot_codex = nested.join(".codex");
tokio::fs::create_dir_all(&nested_dot_codex).await?;
tokio::fs::create_dir_all(project_root.join(".git")).await?;
tokio::fs::write(nested_dot_codex.join(CONFIG_TOML_FILE), "foo = \"child\"\n").await?;
tokio::fs::create_dir_all(&project_dot_codex).await?;
make_config_for_test(&project_dot_codex, &project_root, TrustLevel::Trusted, None).await?;
let user_config_path = project_dot_codex.join(CONFIG_TOML_FILE);
let user_config_contents = tokio::fs::read_to_string(&user_config_path).await?;
tokio::fs::write(
&user_config_path,
format!("foo = \"user\"\n{user_config_contents}"),
)
.await?;
let cwd = AbsolutePathBuf::from_absolute_path(&nested)?;
let layers = load_config_layers_state(
&project_dot_codex,
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
)
.await?;
let project_layers: Vec<_> = layers
.get_layers(
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
true,
)
.into_iter()
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
.collect();
let child_config: TomlValue = toml::from_str("foo = \"child\"\n").expect("parse child config");
assert_eq!(
vec![&ConfigLayerEntry {
name: super::ConfigLayerSource::Project {
dot_codex_folder: AbsolutePathBuf::from_absolute_path(&nested_dot_codex)?,
},
config: child_config.clone(),
version: version_for_toml(&child_config),
disabled_reason: None,
}],
project_layers
);
assert_eq!(
layers.effective_config().get("foo"),
Some(&TomlValue::String("child".to_string()))
);
Ok(())
}
#[tokio::test]
async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<()> {
let tmp = tempdir()?;