Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,41 @@ fn resolve_codex_home(cli_codex_home: Option<PathBuf>) -> Result<PathBuf> {
resolve_codex_home_from_env(
cli_codex_home,
std::env::var_os("CODEX_HOME").map(PathBuf::from),
std::env::var_os("HOME").map(PathBuf::from),
home_base_from_env(
std::env::var_os("USERPROFILE").map(PathBuf::from),
std::env::var_os("HOME").map(PathBuf::from),
cfg!(windows),
),
)
}

/// Resolve the base directory the default `.codex` home hangs off of when
/// neither `--codex-home` nor `CODEX_HOME` is set.
///
/// This mirrors how Codex itself locates its home via `dirs::home_dir()`: on
/// Windows that resolves from `USERPROFILE` and never consults `HOME`, while on
/// Unix it uses `HOME`. threadripper historically looked only at `HOME`, so any
/// process that injects `HOME` (e.g. an app pointing it at `%APPDATA%\<app>`)
/// sent us to the wrong `.codex` while Codex kept writing under
/// `%USERPROFILE%\.codex`. Preferring `USERPROFILE` on Windows realigns the two;
/// `HOME` stays as a fallback there for shells that only set it (Git Bash, MSYS).
fn home_base_from_env(
userprofile: Option<PathBuf>,
home: Option<PathBuf>,
is_windows: bool,
) -> Option<PathBuf> {
fn non_empty(path: PathBuf) -> Option<PathBuf> {
(!path.as_os_str().is_empty()).then_some(path)
}
if is_windows {
userprofile
.and_then(non_empty)
.or_else(|| home.and_then(non_empty))
} else {
home.and_then(non_empty)
}
}

fn resolve_codex_home_from_env(
cli_codex_home: Option<PathBuf>,
env_codex_home: Option<PathBuf>,
Expand Down
8 changes: 6 additions & 2 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,12 @@ pub(crate) fn bytes_value_name(locale: Locale) -> &'static str {

pub(crate) fn codex_home_help(locale: Locale) -> &'static str {
match locale {
Locale::En => "Codex home directory. Defaults to CODEX_HOME, then $HOME/.codex.",
Locale::ZhHans => "Codex home 目录。默认值依次使用 CODEX_HOME、$HOME/.codex。",
Locale::En => {
"Codex home directory. Defaults to CODEX_HOME, then <home>/.codex (USERPROFILE on Windows, else HOME)."
}
Locale::ZhHans => {
"Codex home 目录。默认依次使用 CODEX_HOME、家目录下的 .codex(Windows 取 USERPROFILE,其余取 HOME)。"
}
}
}

Expand Down
59 changes: 59 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,65 @@ fn resolves_codex_home_from_cli_then_env_then_home() -> Result<()> {
Ok(())
}

#[test]
fn home_base_prefers_userprofile_on_windows() -> Result<()> {
// Windows: USERPROFILE wins and HOME is ignored even when both are set —
// this is the reported regression (HOME hijacked to %APPDATA%\<app>).
assert_eq!(
crate::home_base_from_env(
Some(PathBuf::from(r"C:\Users\alice")),
Some(PathBuf::from(r"C:\Users\alice\AppData\Roaming\SPB_16.6")),
true,
),
Some(PathBuf::from(r"C:\Users\alice")),
);
// Windows: fall back to HOME when USERPROFILE is missing or empty.
assert_eq!(
crate::home_base_from_env(None, Some(PathBuf::from(r"C:\home")), true),
Some(PathBuf::from(r"C:\home")),
);
assert_eq!(
crate::home_base_from_env(Some(PathBuf::new()), Some(PathBuf::from(r"C:\home")), true),
Some(PathBuf::from(r"C:\home")),
);
// Windows: nothing usable -> None (caller then defaults to ".").
assert_eq!(crate::home_base_from_env(None, None, true), None);
assert_eq!(
crate::home_base_from_env(Some(PathBuf::new()), None, true),
None,
);

// Non-Windows: HOME is authoritative and USERPROFILE is ignored.
assert_eq!(
crate::home_base_from_env(
Some(PathBuf::from("/should/ignore")),
Some(PathBuf::from("/home/alice")),
false,
),
Some(PathBuf::from("/home/alice")),
);
assert_eq!(
crate::home_base_from_env(Some(PathBuf::from("/should/ignore")), None, false),
None,
);

// End-to-end: with HOME hijacked but USERPROFILE intact, the default Codex
// home still resolves under the real user profile, not the app dir.
assert_eq!(
crate::resolve_codex_home_from_env(
None,
None,
crate::home_base_from_env(
Some(PathBuf::from(r"C:\Users\alice")),
Some(PathBuf::from(r"C:\Users\alice\AppData\Roaming\SPB_16.6")),
true,
),
)?,
PathBuf::from(r"C:\Users\alice").join(".codex"),
);
Ok(())
}

#[test]
fn resolves_sqlite_path_from_config_sqlite_home() -> Result<()> {
let dir = tempfile::tempdir()?;
Expand Down
Loading