find_codex_home() silently ignores a non-UTF-8 CODEX_HOME and resolves to ~/.codex instead
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 91/100
Research direction
Start in codex-rs/utils/home-dir/src/lib.rs, especially the CODEX_HOME handling at lines 14-16 and the existing environment-path tests. Run cargo test -p codex-utils-home-dir --test lead_repro -- --test-threads=1, then verify that a non-UTF-8 CODEX_HOME is honored when valid or produces an explicit error, without changing existing UTF-8 behavior.
Written by the indexing model from the issue text.
Description
What version of Codex CLI is running?
0.155.0 (npm install, linux-x64). Also reproduced against the source the report was
written from; codex-rs/utils/home-dir/src/lib.rs there is byte-identical to
upstream main (55543d8772, 2026-09-25T15:42Z) — blob
caa43569c78bae9f5cc875092f378f4d935b8063 on both sides.
What subscription do you have?
Not applicable — source-level report, no account interaction is involved.
Which model were you using?
Not applicable — the config-resolution layer is exercised before any model call.
What platform is your computer?
uname -mprs → Linux 7.0.0-31-generic x86_64 unknown
What terminal emulator and version are you using?
Not applicable; non-interactive codex doctor --json.
Codex doctor report
Relevant excerpt is inline below; the remainder is omitted. No secrets are involved.
What issue are you seeing?
find_codex_home() discards CODEX_HOME when its value is not valid UTF-8, and
silently resolves to ~/.codex instead: no error, no warning, and codex doctor
reports config.load: ok.
CODEX_HOME is read with std::env::var, which returns
Err(VarError::NotUnicode) for a non-UTF-8 value. .ok() converts that error
into None, which is indistinguishable from "variable not set", so
find_codex_home_from_env takes the default branch.
Non-UTF-8 path names are legal on Linux and macOS (they routinely appear when
extracting archives with non-UTF-8 names, or from filesystems with mixed
encodings), so this is a real, if uncommon, configuration.
Because this resolver backs config, credentials and session state, the
consequence is that an explicitly requested directory is ignored and a
different one is used — silently.
Observed with a sentinel model value written into both candidate configs, so
the file that was actually loaded is unambiguous:
CODEX_HOME |
config file actually loaded | sentinel model |
|---|---|---|
| unset | $HOME/.codex/config.toml |
FALLBACK-home-codex |
| UTF-8 path, exists | <that dir>/config.toml |
REQUESTED-utf8-home |
| non-UTF-8 path, exists | $HOME/.codex/config.toml |
FALLBACK-home-codex |
codex doctor --json for the non-UTF-8 case — note status=ok, and that the
requested directory is not mentioned anywhere:
[config.load] status=ok summary='config loaded'
CODEX_HOME = '/tmp/repro/home/.codex'
config.toml = '/tmp/repro/home/.codex/config.toml'
model = 'FALLBACK-home-codex'
[state.paths] status=ok summary='state paths and databases are inspectable'
CODEX_HOME = '/tmp/repro/home/.codex (dir)'
The requested directory /tmp/repro/non-utf8-\xff existed throughout and
contained a valid config.toml with model = "REQUESTED-nonutf8-home".
What steps can reproduce the bug?
Invoke the native binary directly. ($HOME is redirected so that the
fallback target is a scratch directory; nothing outside the temp tree is
read or written.)
import os, subprocess, tempfile
NATIVE = ("<install root>/node_modules/@openai/codex-linux-x64"
"/vendor/x86_64-unknown-linux-musl/bin/codex")
scratch = tempfile.mkdtemp(prefix="repro-")
home = os.path.join(scratch, "home")
os.makedirs(os.path.join(home, ".codex"))
with open(os.path.join(home, ".codex", "config.toml"), "w") as f:
f.write('model = "FALLBACK-home-codex"\n')
requested = os.fsencode(scratch) + b"/non-utf8-\xff" # not valid UTF-8
os.mkdir(requested)
with open(os.path.join(requested, b"config.toml"), "wb") as f:
f.write(b'model = "REQUESTED-nonutf8-home"\n')
env = dict(os.environ, HOME=home, CODEX_HOME=os.fsdecode(requested))
out = subprocess.run([NATIVE, "doctor", "--json"], capture_output=True,
env=env, cwd=scratch).stdout.decode("utf-8", "replace")
print("requested:", requested)
for line in out.splitlines():
if "config.toml" in line or '"model"' in line:
print(" ", line.strip())
Observed output:
requested: b'/tmp/repro-xxxxxxxx/non-utf8-\xff'
"config.toml": "/tmp/repro-xxxxxxxx/home/.codex/config.toml",
"model": "FALLBACK-home-codex",
The requested directory is ignored. With a UTF-8 name in the same position, the
requested directory is honored.
The same behavior can be reproduced at unit level in
codex-rs/utils/home-dir, where the resolver lives:
let mut raw = base.into_os_string().into_vec();
raw.extend_from_slice(b"/codex-home-\xff");
let dir = std::path::PathBuf::from(std::ffi::OsString::from_vec(raw));
std::fs::create_dir_all(&dir).unwrap();
unsafe { std::env::set_var("CODEX_HOME", dir.as_os_str()) }; // edition 2024
assert_eq!(
codex_utils_home_dir::find_codex_home().unwrap().as_path(),
dir.canonicalize().unwrap(),
); // fails: resolved to $HOME/.codex
$ cargo test -p codex-utils-home-dir --test lead_repro -- --test-threads=1
control_utf8_codex_home_is_honored ... ok
subject_non_utf8_codex_home_existing_dir_is_ignored ... FAILED
left: "/tmp/repro-xxxxxxxx/home/.codex"
right: "/tmp/repro-xxxxxxxx/codex-home-\xFF"
What is the expected behavior?
The function's own documentation (codex-rs/utils/home-dir/src/lib.rs:5-12)
states the contract:
- If
CODEX_HOMEis set, the value must exist and be a directory. The value
will be canonicalized and this function will Err otherwise.
A non-UTF-8 value therefore has two acceptable outcomes, and the current
behavior implements neither: resolve to the directory when it exists and is a
directory, or return an error naming CODEX_HOME. Silently substituting
~/.codex is the one outcome the documented contract rules out. The crate's
existing tests encode the same intent for UTF-8 values
(find_codex_home_env_missing_path_is_fatal,
find_codex_home_env_file_path_is_fatal).
This also defeats the validation added for CODEX_HOME in #10249 ("Validate
CODEX_HOME before resolving"), which is the commit that introduced the
env::var(...).ok() chain — so the fail-loudly guarantee added there does not
hold for this input class.
Additional information
Root cause. codex-rs/utils/home-dir/src/lib.rs:14-16:
let codex_home_env = std::env::var("CODEX_HOME")
.ok()
.filter(|val| !val.is_empty());
VarError::NotUnicode is collapsed into None by .ok(), so the Some(val)
branch at :24, including the "path does not exist" error at :29, is never
reached for such values.
Possible fix. Read with std::env::var_os("CODEX_HOME") and keep the
empty-value filter; pass the OsString/PathBuf through so that non-UTF-8
directories resolve normally, or return an explicit error when the value is not
representable. Other CODEX_HOME readers in the tree already use var_os —
config/src/codex_home_symlink.rs:23 and cli/src/doctor/disk.rs:19
(path-utils/src/env.rs:7 does the same for WSL_DISTRO_NAME) — so this read
is the outlier.
Blast radius. find_codex_home() has 47 non-test call sites, including core
config loading, rmcp-client OAuth storage and refresh locks, network-proxy
CA storage, session/rollout stores, sandboxing and arg0. The requested home is
not merely displayed incorrectly; it is not used.
Launcher note (separate from the defect above). With the npm launcher the
symptom looks different, because the value is mangled before codex starts:
/usr/local/bin/codex is a Node script that does const env = { ...process.env }
and then spawn(binaryPath, argv, { env }). Node decodes environment values as
UTF-8, so 0xFF becomes U+FFFD and the child receives a different,
non-existent path, producing:
CODEX_HOME points to "...non-utf8-\uFFFD", but that path does not exist
That loud error is why the silent fallback is easy to miss for npm users. The
fallback itself is observable whenever the native binary is invoked directly, or
by any launcher that preserves the bytes.
Not a security report. No sandbox, permission or approval bypass, and no
attacker-controlled input. It is worth a look only because the auth store is
among the directories that can be silently relocated.
Duplicate search. Searched open and closed issues for CODEX_HOME,
CODEX_HOME non-UTF-8, NotUnicode, find_codex_home, invalid unicode environment variable and CODEX_HOME ignored. Nothing covers this mechanism.
The closest existing reports are #45871 (find_codex_home()'s canonicalize()
denied inside a Windows AppContainer — same function and the same class of "a
valid CODEX_HOME is not honored", different mechanism), #46787 and #35310
(symlinked CODEX_HOME), and #27765 (deep CODEX_HOME path).
Verified on Linux only. Windows environment values are UTF-16; an unpaired
surrogate would take the same NotUnicode path, but I could not test that here.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.5k
- Avg merge
- 1m
- Merged PRs (30d)
- 996
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openai/codex
-
app bug CLI TUI
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
openai/codex#48433 · 1 comment ·
Maintainers usually reply within 1 day
-
Docs: "Work with Codex from anywhere" page still claims Windows mobile support is "coming soon"Openapp documentation remote windows-os
Difficulty 1/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
app CLI enhancement TUI windows-os
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
bug CLI model-behavior
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
openai/codex#48093 · 1 reaction ·
Maintainers usually reply within 1 day
-
auth bug mcp
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
openai/codex#48037 · 1 comment ·
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
trailofbits/dylint#2107 ·
Maintainers usually reply within 1 day
-
area:cli bug good first issue priority:medium
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
Maintainers usually reply within 1 day
-
arrays_zip with two same-named inputs fails with "ArrowArray struct has 2 children (expected 1)"Openbug requires-triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
apache/datafusion-comet#6251 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
bug false-positive harper-core linting
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Automattic/harper#4471 ·
Maintainers usually reply within 1 day