Summary
When CODEX_HOME is set and codex runs inside a Windows AppContainer (lowbox token), startup fails with:
WARNING: proceeding, even though we could not create PATH aliases: failed to canonicalize CODEX_HOME "<dir>": Access is denied. (os error 5)
Error loading configuration: failed to canonicalize CODEX_HOME "<dir>": Access is denied. (os error 5)
even though the process can otherwise read and write that same directory. The cause is find_codex_home() calling
PathBuf::from(val).canonicalize() on CODEX_HOME. std::fs::canonicalize on Windows resolves the final path via
GetFinalPathNameByHandleW(..., VOLUME_NAME_DOS), and an AppContainer token cannot resolve the C:\ DOS drive
letter because that requires access to \GLOBAL??\C: (the DosDevices symbolic link in the object namespace), which
lowbox tokens are denied. It is not a filesystem-ACL problem — granting the package/capability SID or ALL APPLICATION PACKAGES on the directory (or the drive root) does not help.
This matters because per-agent isolation on Windows (e.g. running multiple codex instances each confined to its own
CODEX_HOME inside its own AppContainer — the same AppContainer approach OpenAI's own Codex Windows sandbox uses) is
exactly the case that must set CODEX_HOME, and it's the only find_codex_home() branch that canonicalizes.
Root cause (isolated empirically)
Inside an AppContainer, we decomposed what canonicalize does with a native probe on the process's own
(read/writable) directory:
| Step |
API |
Result inside AppContainer |
| Open the dir |
CreateFileW(access=0, FILE_FLAG_BACKUP_SEMANTICS) |
OK |
| Resolve NT path |
GetFinalPathNameByHandleW(VOLUME_NAME_NT) |
OK (\Device\HarddiskVolumeN\…) |
| Resolve DOS path |
GetFinalPathNameByHandleW(VOLUME_NAME_DOS) |
FAIL — Access denied (needs \GLOBAL??\C:) |
So the dir open succeeds and NT-path resolution succeeds; only the DOS drive-letter resolution (which
std::fs::canonicalize uses) is denied. Plain std::fs::read/write on the same dir work — they don't do the
\GLOBAL?? resolution, so only canonicalize trips it.
The offending call is codex-rs/core/src/config.rs, find_codex_home():
pub fn find_codex_home() -> std::io::Result<PathBuf> {
if let Ok(val) = std::env::var("CODEX_HOME") {
if !val.is_empty() {
return PathBuf::from(val).canonicalize(); // <-- fails inside an AppContainer
}
}
// ... (the unset branch does not canonicalize)
}
Proposed fix (one-liner, no behavior change on normal setups)
Don't hard-fail on canonicalize; fall back to a pure-string absolutization that opens no handle and does no
\GLOBAL?? resolution. This is the well-established try_canonicalize pattern (used by cargo/rustc for exactly this
class of Windows/sandbox canonicalize unreliability — see rust-lang/rust#79449, cargo#11866, std::path::absolute
rust-lang/rust#91673):
if let Ok(val) = std::env::var("CODEX_HOME") {
if !val.is_empty() {
let p = PathBuf::from(val);
// canonicalize() opens a handle + resolves the DOS drive letter via \GLOBAL??, which a
// Windows AppContainer/lowbox token is denied. Fall back to a pure-string absolute path
// (GetFullPathNameW on Windows) so CODEX_HOME works inside a sandbox.
return p.canonicalize().or_else(|_| std::path::absolute(&p));
}
}
std::path::absolute (stable since 1.79) is GetFullPathNameW-based on Windows — no directory handle, no object-
namespace lookup — so it succeeds in the AppContainer while remaining a no-op-equivalent for already-absolute paths on
normal setups. (If std::path::absolute isn't desired, dunce::canonicalize or a manual GetFullPathNameW are
equivalents; the key point is: don't require a handle-opening canonicalize on CODEX_HOME.)
Repro (Windows 11)
- Create an AppContainer profile (
CreateAppContainerProfile) → package SID.
- Make a dir (e.g.
C:\codex-home-test) and grant that package SID Full (so the process can read/write it), seed a
normal ~/.codex into it, set CODEX_HOME to it.
- Launch
codex login status in the AppContainer via CreateProcess + PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES.
- Observe the
failed to canonicalize CODEX_HOME … Access is denied (os error 5) error, despite the process being
able to read/write the dir. GetFinalPathNameByHandleW(VOLUME_NAME_NT) on the same handle succeeds; VOLUME_NAME_DOS
fails.
Related
Environment
- Windows 11 (10.0.22631), codex CLI (Rust), running inside a per-agent AppContainer (lowbox token).
Summary
When
CODEX_HOMEis set and codex runs inside a Windows AppContainer (lowbox token), startup fails with:even though the process can otherwise read and write that same directory. The cause is
find_codex_home()callingPathBuf::from(val).canonicalize()onCODEX_HOME.std::fs::canonicalizeon Windows resolves the final path viaGetFinalPathNameByHandleW(..., VOLUME_NAME_DOS), and an AppContainer token cannot resolve theC:\DOS driveletter because that requires access to
\GLOBAL??\C:(the DosDevices symbolic link in the object namespace), whichlowbox tokens are denied. It is not a filesystem-ACL problem — granting the package/capability SID or
ALL APPLICATION PACKAGESon the directory (or the drive root) does not help.This matters because per-agent isolation on Windows (e.g. running multiple codex instances each confined to its own
CODEX_HOMEinside its own AppContainer — the same AppContainer approach OpenAI's own Codex Windows sandbox uses) isexactly the case that must set
CODEX_HOME, and it's the onlyfind_codex_home()branch that canonicalizes.Root cause (isolated empirically)
Inside an AppContainer, we decomposed what
canonicalizedoes with a native probe on the process's own(read/writable) directory:
CreateFileW(access=0, FILE_FLAG_BACKUP_SEMANTICS)GetFinalPathNameByHandleW(VOLUME_NAME_NT)\Device\HarddiskVolumeN\…)GetFinalPathNameByHandleW(VOLUME_NAME_DOS)\GLOBAL??\C:)So the dir open succeeds and NT-path resolution succeeds; only the DOS drive-letter resolution (which
std::fs::canonicalizeuses) is denied. Plainstd::fs::read/writeon the same dir work — they don't do the\GLOBAL??resolution, so onlycanonicalizetrips it.The offending call is
codex-rs/core/src/config.rs,find_codex_home():Proposed fix (one-liner, no behavior change on normal setups)
Don't hard-fail on
canonicalize; fall back to a pure-string absolutization that opens no handle and does no\GLOBAL??resolution. This is the well-establishedtry_canonicalizepattern (used by cargo/rustc for exactly thisclass of Windows/sandbox
canonicalizeunreliability — see rust-lang/rust#79449, cargo#11866,std::path::absoluterust-lang/rust#91673):
std::path::absolute(stable since 1.79) isGetFullPathNameW-based on Windows — no directory handle, no object-namespace lookup — so it succeeds in the AppContainer while remaining a no-op-equivalent for already-absolute paths on
normal setups. (If
std::path::absoluteisn't desired,dunce::canonicalizeor a manualGetFullPathNameWareequivalents; the key point is: don't require a handle-opening
canonicalizeonCODEX_HOME.)Repro (Windows 11)
CreateAppContainerProfile) → package SID.C:\codex-home-test) and grant that package SID Full (so the process can read/write it), seed anormal
~/.codexinto it, setCODEX_HOMEto it.codex login statusin the AppContainer viaCreateProcess+PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES.failed to canonicalize CODEX_HOME … Access is denied (os error 5)error, despite the process beingable to read/write the dir.
GetFinalPathNameByHandleW(VOLUME_NAME_NT)on the same handle succeeds;VOLUME_NAME_DOSfails.
Related
access). This is the path-resolution counterpart: even with directory ACLs correct,
canonicalizeonCODEX_HOMEfails on the
\GLOBAL??DOS-drive-letter step.Environment