arg0 error messages lack path and operation context, making failures hard to diagnose

Open 💬 15 comments Opened Apr 26, 2026 by HaleTom

Why is this important?

It's almost impossible to run codex in a bwrap or firejail sandbox without proper diagnostics file access failures

What version of Codex CLI is running?

codex-cli 0.120.0 (built from source at commit bdb62e2c2). Note: this is a pre-existing gap present in all versions that include the arg0 setup path — not a regression.

What subscription do you have?

N/A (local build)

Which model were you using?

N/A — this is a diagnosability/tooling issue, not model-specific

What platform is your computer?

Linux 6.x x86_64

What terminal emulator and version are you using (if applicable)?

N/A — reproduced in headless/session mode

What issue are you seeing?

When prepend_path_entry_for_codex_aliases() fails during arg0 setup (permission errors, stale lock files, missing temp directories), every ? operator propagates a bare std::io::Error with only the OS-level message — e.g. "No such file or directory" or "Permission denied" — with no indication of which path or which operation failed.

This is a pre-existing diagnosability gap (not a regression) that makes the arg0-related failures reported in #16970 and #17240 extremely difficult to triage because:

  • The error doesn't identify the path that was being accessed
  • The error doesn't identify which operation (create directory, open lock file, acquire lock, write symlink) was being attempted
  • On multi-user systems, a stale lock file owned by another user produces "Permission denied" with no path — completely opaque
  • The worst offender is lock_file.try_lock()? which surfaces fs4::TryLockError with no indication of which lock file or why it failed
  • Users and operators must attach a debugger or guess from context

Example bare error from the current code:

Io(Os { code: 2, kind: NotFound, message: "No such file or directory" })

What the user needs to see:

open lock file: /home/user/.codex/tmp/arg0/codex-arg0XXX/.lock: No such file or directory

What steps can reproduce the bug?

Arg0 failures are inherently intermittent (race conditions, stale temp dirs, permission changes), but these sequences reliably reproduce the bare-error symptom:

Scenario A — missing arg0 directory:

  1. Start Codex CLI in a session where ~/.codex/tmp/arg0/ does not exist
  2. Trigger a tool call that requires arg0 path setup (any exec_command or apply_patch invocation)
  3. Observe a bare "No such file or directory" error with no path context

Scenario B — stale temp directory (simulates #16970):

  1. Delete a codex-arg0* temp directory while a Codex session is active
  2. Trigger another tool call
  3. Same bare error with no way to identify which temp path or lock file failed

Scenario C — permission denied on lock file:

  1. Create ~/.codex/tmp/arg0/codex-arg0XXX/.lock owned by another user
  2. Start Codex in a new session
  3. Observe bare "Permission denied" with no indication of which file

Note: these repro steps trigger the symptom class (bare errors), not a single deterministic failure path. The root causes of the failures in #16970 and #17240 are separate bugs; this issue is about the diagnosability gap.

What is the expected behavior?

Error messages from the arg0 setup path should identify both the operation being attempted and the path involved, e.g.:

  • "create directory: /home/user/.codex/tmp/arg0: Permission denied"
  • "open lock file: /home/user/.codex/tmp/arg0/codex-arg0XXX/.lock: No such file or directory"
  • "acquire lock: /home/user/.codex/tmp/arg0/codex-arg0XXX/.lock: Permission denied"
  • "write symlink: /home/user/.codex/tmp/arg0/codex-arg0XXX/apply_patch: No such file or directory"

Additional information

Related bug reports whose diagnosability would be improved by this change:

  • #16970 — Unified exec caches stale ~/.codex/tmp/arg0 session path and fails with ENOENT
  • #17240 — Codex tool runtime intermittently loses apply_patch path and fails with No such file or directory

Important: this is a diagnosability improvement, not a fix for the underlying stale-path bugs in #16970 / #17240. Those bugs need separate fixes (e.g., arg0 path revalidation on ENOENT). This issue is specifically about making the error messages actionable so that users and maintainers can identify which path and operation failed without needing a debugger.

Suggested approach: wrap all std::io::Error sites in prepend_path_entry_for_codex_aliases() with a with_path_context(error, path, operation) helper that prepends the operation and path to the error message. A working implementation is on branch HaleTom:codex:enhance-arg0-error-context.

View original on GitHub ↗

15 Comments

HaleTom · 2 months ago

@etraut-openai A CLA-signed PR implementing this is ready for review: HaleTom/codex#1:

Copilot reviewed 1 out of 2 changed files in this pull request and generated no new comments.

It wraps every ? site in prepend_path_entry_for_codex_aliases() with with_path_context() / with_context() helpers.

I'd use the a PR template, but I couldn't find one: No PR template exists -- contributing.md and PR template form a circular reference with no content #19856

joshka · 2 months ago

A better solution to this more generally would be to swap in the fs_err crate for all std::fs calls and add an AGENTS.md guidance for this and a lint + config that prevents std::fs methods from being used
#![deny(clippy::disallowed_methods)]

HaleTom · 2 months ago

Thanks for the fs-err direction — you're right that it's cleaner than the current PR.

I implemented a hybrid approach: fs-err for all normal filesystem operations, and a narrow local helper only for the lock-acquire site where fs-err can't help.

What fs-err covers (now adopted in codex-arg0):

fs_err::create_dir_all(&temp_root)?;           // was std::fs::create_dir_all
fs_err::read_dir(temp_root)?;                  // was std::fs::read_dir
fs_err::remove_dir_all(&path)?;                // was std::fs::remove_dir_all
fs_err::set_permissions(&temp_root, Permissions::from_mode(0o700))?;  // was
fs_err::os::unix::fs::symlink(&exe, &link)?;  // was std::os::unix::fs::symlink
fs_err::write(&batch_script, ...)?;            // was std::fs::write

The remaining site that fs-err can't cover — lock acquire:

try_lock() returns TryLockError, not io::Error. fs-err has no lock API. I added a tiny local helper that centralizes the arg0 lock policy:

fn try_lock_arg0_dir(lock_file: &File, lock_path: &Path) -> io::Result<()> {
    match lock_file.try_lock() {
        Ok(()) => Ok(()),
        // During startup: WouldBlock = lock already held by another process → real error
        Err(std::fs::TryLockError::WouldBlock) => Err(with_path_context(
            io::Error::new(io::ErrorKind::AlreadyExists,
                format!("lock is already held: {}", lock_path.display())),
            lock_path, "acquire lock",
        )),
        // Unexpected lock failure
        Err(err) => Err(with_path_context(io::Error::from(err), lock_path, "acquire lock")),
    }
}

The janitor uses a separate try_lock_dir() that treats WouldBlock as Ok(None) — it expects to encounter held locks during cleanup. The main path uses try_lock_arg0_dir() which treats WouldBlock as a real error with AlreadyExists kind and full path context.

The overall approach:

  1. fs-err for the broad portable filesystem surface → path-rich errors automatically
  2. try_lock_arg0_dir for the one site fs-err can't reach → domain-specific error enrichment
  3. with_path_context kept only for lock acquire and the handful of non-portable/fs-err-covered sites

Pushed to the PR as enhance-arg0-error-context branch. Still needs a follow-up PR to add clippy::disallowed-methods entries for the raw std::fs methods you suggested, once this one lands.

HaleTom · 2 months ago

The std::fs migration across all 1041 call sites in ~20 crates should probably be a separate PR (or series of PRs).

This PR focuses specifically on codex-arg0, which was the crate producing the noisy errors discussed in the issue. codex-arg0 is now fully migrated: all production filesystem ops use fs_err, and an inline clippy::disallowed_methods lint prevents future regressions in this crate.

A workspace-wide migration would be a larger effort because:

  • Many hits are in test code where fs_err adds less value
  • Some std::fs::File + OpenOptions patterns need per-crate review for fs_err::File API differences
  • fs_err lacks equivalents for std::fs::create_dir (only create_dir_all) and lock-file operations
  • The workspace-wide change was intentionally scoped to codex-arg0 to avoid affecting other crates that may have different migration timelines

Happy to take that on as a follow-up PR if desired.

HaleTom · 2 months ago

@joshka @etraut-openai would you please take a look at https://github.com/HaleTom/codex/pull/1 when you get a chance.

I've done what I'd consider due diligence on it and think it's ready for a merge. Cheers!

joshka · 2 months ago

@HaleTom I'm only an casually interested observer on this one as I have seen enough of such error messages without filenames to know that fs_err is a reasonable silver bullet here that should be used generally across any application code, but I'll leave it with @etraut-openai to work out what to do with this.

To be clear:
As a general rule, error messages should contain the filename involved in the error.
There's a general class of problems where knowing the filename involved in the problem is the most useful information that helps diagnosis and not knowing it makes it significantly harder (i.e. you have to read the source to understand what the filename should be). There are likely exceptions to this rule and there are existing code paths where this information is already surfaced. Changing this across codex would be easy, but reviewing it is hard as it touches many things.

HaleTom · 2 months ago

I'm very happy to keep the scope constrained to only fs_err to reduce the amount of change in other crates.

Hard agree that error messages without sufficient infomation as to what caused the error are almost impossible to diagnose. Rhetorically: what's the problem with more information rather than less in the very, very few exceptional cases?

And sandboxes will always cause exceptional cases :)

joshka · 2 months ago
Rhetorically: what's the problem with more information rather than less in the very, very few exceptional cases?

Privacy is the most obvious constraint. A few others:

  • Sensitive information exposure: paths or args can include user names, project names, or secrets that shouldn’t end up in logs.
  • Duplication: if the underlying error already includes the path, repeating it just adds noise.
  • Clarity: extra context can mislead if the path is incidental to the failure.
  • Signal vs noise: overly verbose errors are harder to scan.
  • Abstraction boundaries: low-level details (e.g. temp paths) can leak internals that don’t help the caller.
  • Stability: including paths in strings can make tests and log parsing brittle.

A reasonable rule:

Error messages should include the path or resource identifier when it materially improves diagnosability, unless it reduces clarity, duplicates information, or exposes sensitive details.

In the arg0 case, the paths are local temp/cache paths already implicated in the failure, so including them seems like a clear win.

More broadly, this feels like a good fit for an agent pass. Codex can do the mechanical sweep, adding context where it helps and leaving the rest alone.

The real cost is review, not implementation. These are mostly unhappy paths, so the payoff is lower than spending the same effort on hot paths. That’s why a targeted, crate-by-crate pass makes sense: broad automation, focused review where it matters.

HaleTom · 2 months ago

Production Readiness Review: enhance-arg0-error-context

PR: HaleTom/codex#1 (branch: enhance-arg0-error-context)
Reviewed: 2026-05-19T17:32:10+07:00
Scope: 5 files changed, +269 / -40 (codex-arg0 crate only)

Context files read

  • codex-rs/arg0/src/lib.rs:1-781 — full current file (main changed file)
  • /tmp/upstream-arg0-lib.rs:1-585 — upstream version for diff comparison
  • codex-rs/arg0/clippy.toml:1-22 — new file, disallowed-methods lint config
  • codex-rs/arg0/Cargo.toml:1-26 — added fs-err dependency
  • codex-rs/Cargo.toml:254-261 — workspace-level fs-err dependency
  • codex-rs/Cargo.lock — fs-err 3.3.0 added, transitive dep changes
  • GitHub issue openai/codex#19674 comments (all 8 comments) — design intent and scope discussion

Verification evidence

  • cargo check -p codex-arg0passed (1m 29s)
  • cargo clippy -p codex-arg0passed (0 warnings, 1m 09s)
  • cargo test -p codex-arg08/8 passed (0.00s)

---

Production-ready bar for this PR

  1. All std::fs calls in production code replaced with fs_err equivalents — errors must include the file path
  2. Lock-acquire failures include the lock file path and "acquire lock" operation context
  3. ContextIoError preserves the Error::source() chain so anyhow and downstream error walkers see the original OS error
  4. deep_raw_os_error correctly walks the source chain to recover raw_os_error() lost by io::Error::new
  5. clippy::disallowed_methods lint prevents future std::fs regression in this crate
  6. No duplicate path information in error messages — with_context for fs_err calls (path already in message), with_path_context only for non-fs_err sites
  7. Public API change (prepend_path_entry_for_codex_aliases now takes exe: Option<&Path>) is intentional and the single caller is updated
  8. current_exe() hoisted out of the per-filename loop — eliminates redundant syscalls
  9. Tests cover all new helper functions and the source-chain walking behavior
  10. No blocking clippy warnings, no new unsafe code introduced

---

Findings

1. Correctness & functional completeness

Finding 1.1 — with_path_context doc comment is misleading about with_context usage

  • Classification: NIT
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:334-339
  • Confidence: High

The doc says:

For fs_err calls that already include the path in their error message, prefer [with_context] instead to avoid duplicating the path in the output.

This reads as if with_context is for wrapping errors returned by fs_err calls. But with_context is also used for fs_err calls themselves (e.g., line 403: fs::create_dir_all(&codex_home).map_err(|e| with_context(e, "create CODEX_HOME directory"))). The sentence is technically correct but confusing. Recommend rewording to:

For errors from fs_err operations (which already include the path), prefer [with_context] to add operation context without duplicating the path.

Finding 1.2 — Source chain is correctly preserved

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:287-303 (ContextIoError), codex-rs/arg0/src/lib.rs:299-302 (Error impl)
  • Confidence: High

ContextIoError implements Error::source() returning Some(&self.source), which means anyhow and other error walkers will traverse: ContextIoError → inner io::Error → original OS error. The deep_raw_os_error function (line 314) correctly walks this chain via downcast_ref::<io::Error>(). Test at line 739 verifies this.

Finding 1.3 — try_lock_arg0_dir error conversion is correct

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:571-580
  • Confidence: High

std::io::Error::from(TryLockError) correctly converts TryLockError::WouldBlock to io::Error with ErrorKind::WouldBlock, and TryLockError::Io(e) extracts the inner error. The with_path_context wrapper then adds the lock path and "acquire lock" operation. Error messages will read: failed to acquire lock '/path/.lock': Resource temporarily unavailable.

Finding 1.4 — current_exe hoisting eliminates redundant syscall

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:435-440 (single call before loop), vs upstream line 339 (current_exe() inside loop)
  • Confidence: High

Upstream called std::env::current_exe() inside the per-filename loop (once per alias). The PR hoists it to a single call before the loop. This is correct — the executable path doesn't change during the function.

2. Architecture & boundary integrity

Finding 2.1 — fs_err migration is correctly scoped

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/Cargo.toml:24 (fs-err dependency), codex-rs/arg0/src/lib.rs:16-19 (imports)
  • Confidence: High

fs_err is imported as use fs_err as fs and use fs_err::File, replacing all std::fs usage in production code. The clippy.toml (22 lines, 19 methods) prevents regression. Test code uses #[allow(clippy::disallowed_methods)] at the module level, which is correct — tests legitimately use std::fs for setup.

Finding 2.2 — Public API change is intentional

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:385-387 (new signature), codex-rs/arg0/src/lib.rs:149-150 (caller updated)
  • Confidence: High

prepend_path_entry_for_codex_aliases changed from fn() to fn(exe: Option<&Path>). The single internal caller (arg0_dispatch) is updated. The arg0_dispatch_or_else function (line 197-200) also reuses the guard's exe when available, avoiding a redundant syscall.

Finding 2.3 — Two lock functions with distinct semantics

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:546-563 (try_lock_dir for janitor), codex-rs/arg0/src/lib.rs:565-580 (try_lock_arg0_dir for startup)
  • Confidence: High

try_lock_dir (janitor) treats WouldBlock as Ok(None) — expected during cleanup. try_lock_arg0_dir (startup) treats WouldBlock as a real error — unexpected during normal startup. This separation is clean and correct.

3. Code clarity, clean code & maintainability

Finding 3.1 — ContextIoError is minimal and correct

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:287-303
  • Confidence: High

The struct has exactly two fields (context: String, source: io::Error), implements Display as "{context}: {source}", and implements Error::source() correctly. No unnecessary complexity.

Finding 3.2 — Error context strings are descriptive

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:388-464 (all .map_err calls)
  • Confidence: High

Every map_err call uses a distinct, descriptive context string:

  • "resolve CODEX_HOME" (line 388)
  • "create CODEX_HOME directory" (line 403)
  • "create temp directory" (line 405)
  • "set permissions on temp directory" (line 411)
  • "create temp directory for arg0" (line 422)
  • "open lock file" (line 432)
  • "get current executable path" (line 438)
  • "create symlink" (line 453)
  • "write batch script" (line 464)

Each uniquely identifies the failing operation.

4. Comments & code documentation

Finding 4.1 — with_path_context doc comment wording (duplicate of 1.1)

  • Classification: NIT
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:334-339
  • Confidence: High

Same as Finding 1.1. The sentence "prefer [with_context] instead" is ambiguous about whether it means "for errors returned by fs_err" or "for fs_err calls themselves." Recommend clarifying.

Finding 4.2 — deep_raw_os_error doc comment is clear

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:305-312
  • Confidence: High

The doc clearly explains why io::Error::new clears raw_os_error(), how wrapper types are skipped, and how downcast_ref is used. The #[allow(dead_code)] annotation (line 313) is justified — the function is exercised by tests (line 739) and will be used by callers that need the raw OS code.

Finding 4.3 — clippy.toml comments explain exceptions

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:2-6 (inline comments at top)
  • Confidence: High

The crate-level #![deny(clippy::disallowed_methods)] has inline comments explaining the three exception categories: TryLockError (no fs_err lock API), Permissions (type constructor), and test-only usage.

5. Tests & validation

Finding 5.1 — New tests cover all helper functions

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:713-780
  • Confidence: High

Three new tests:

  1. with_path_context_includes_operation_path_and_source (line 714) — verifies operation, path, original message, and source chain
  2. deep_raw_os_error_retrieves_original_os_code (line 739) — verifies raw_os_error() is None on wrapper, deep_raw_os_error recovers it
  3. with_context_includes_custom_message_and_source (line 756) — verifies context string, original message, and source chain

Finding 5.2 — Existing tests pass unchanged

  • Classification: PASS
  • Type: Verified issue
  • Evidence: cargo test -p codex-arg0 → 8/8 passed
  • Confidence: High

All 5 existing tests pass: linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias, run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns, janitor_skips_dirs_without_lock_file, janitor_skips_dirs_with_held_lock, janitor_removes_dirs_with_unlocked_lock.

Finding 5.3 — No test for with_context wrapping of fs_err errors

  • Classification: NIT
  • Type: Plausible risk
  • Evidence: No test exercises with_context(e, "...") where e came from an actual fs_err call
  • Confidence: Low

The tests use synthetic io::Error::new(...) values. A test that creates a real fs_err error (e.g., fs_err::read_to_string("/nonexistent")) and wraps it with with_context would verify the combined output format. This is low risk since the wrapping is straightforward, but would add confidence about the "no duplicate path" property.

6. Performance

Finding 6.1 — current_exe hoisting saves syscalls

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:435-440 vs upstream line 339
  • Confidence: High

Upstream called current_exe() once per filename in the loop (4 times on Linux, 2 on Windows). The PR calls it once. Each current_exe() is a syscall (readlink /proc/self/exe on Linux). This is a minor but correct optimization.

Finding 6.2 — ContextIoError overhead is on error paths only

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:345-354 (with_path_context), codex-rs/arg0/src/lib.rs:361-369 (with_context)
  • Confidence: High

The ContextIoError wrapper allocates a String for the context, but only on the error path. Happy path has zero overhead from the context wrappers.

7. Operational risk

Finding 7.1 — fs_err migration is contained to codex-arg0

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/clippy.toml (prevents regression), codex-rs/arg0/Cargo.toml (dependency)
  • Confidence: High

The fs_err dependency and clippy lint are scoped to codex-arg0 only. Other crates are unaffected. The issue discussion confirms this is intentional — workspace-wide migration is deferred to follow-up PRs.

Finding 7.2 — Error messages now include paths (privacy consideration)

  • Classification: NON-BLOCKING
  • Type: Plausible risk
  • Evidence: All .map_err calls in prepend_path_entry_for_codex_aliases
  • Confidence: Medium

Error messages now include filesystem paths (e.g., CODEX_HOME path, temp directory path). The issue discussion (comment #4362753988) acknowledges this: "In the arg0 case, the paths are local temp/cache paths already implicated in the failure, so including them seems like a clear win." This is acceptable for CLI error output but worth noting — these paths would appear in eprintln! output visible to the user.

Finding 7.3 — #![deny(clippy::disallowed_methods)] is crate-level

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:1
  • Confidence: High

The deny is at crate level, enforced by clippy.toml. Test code uses #[allow(clippy::disallowed_methods)] at the module level. This is the correct pattern — production code is linted, test code is exempt.

8. Adversarial review

Adversarial 1 — Could ContextIoError break error downcasting?

Attack: If downstream code does err.downcast_ref::<io::Error>() on the wrapped error, it would get None because the outer type is ContextIoError, not io::Error.

Assessment: Low risk. The crate returns std::io::Result<...>, and the callers use ? or anyhow. anyhow walks the source chain, so the original io::Error is reachable. The raw_os_error() loss is documented and deep_raw_os_error provides recovery. No code in this crate downcasts the returned errors.

Adversarial 2 — Could the exe parameter to prepend_path_entry_for_codex_aliases be None when it shouldn't be?

Attack: If a caller passes None and current_exe() fails, the function returns an error.

Assessment: Correct behavior. The caller (arg0_dispatch) does let exe = std::env::current_exe().ok() and passes exe.as_deref(). If current_exe() fails, exe is None, and the function falls back to calling current_exe() again (line 437-438). If that also fails, the error is propagated with context "get current executable path". This is the correct fallback chain.

Adversarial 3 — Could deep_raw_os_error infinite loop?

Attack: If Error::source() returns a cycle, the while loop would never terminate.

Assessment: Not possible. ContextIoError::source() always returns the inner io::Error, and io::Error::source() returns None for most variants (or a finite chain for custom errors). The standard library Error trait does not support cycles.

Adversarial 4 — Is the with_context call on find_codex_home() correct?

Attack: find_codex_home() returns Result<PathBuf, anyhow::Error>, not io::Error. Does .map_err(|e| with_context(e, ...)) compile?

Assessment: Let me check... find_codex_home returns anyhow::Result<PathBuf>. The .map_err(|e| with_context(e, ...)) would need e to be io::Error. But anyhow::Error is not io::Error.

Wait — this is a real finding. Let me check the actual type at line 388:

let codex_home = find_codex_home().map_err(|e| with_context(e, "resolve CODEX_HOME"))?;

find_codex_home() returns anyhow::Result<PathBuf>. with_context takes std::io::Error. anyhow::Error does not implement Into<std::io::Error>.

However, the code compiled and passed cargo check. So either:

  1. find_codex_home returns io::Result<PathBuf> (not anyhow::Result)
  2. There's an implicit conversion

Let me check... The find_codex_home function is from codex-utils-home-dir. Given the code compiles, it must return io::Result<PathBuf>. This is consistent with the upstream code which uses ? directly (line 285: let codex_home = find_codex_home()?;) where the function returns io::Result.

Verdict: Not an issue. find_codex_home returns io::Result<PathBuf>. The with_context call is type-correct.

---

What I could not fully verify

  1. Workspace-wide impact: Could not verify that no other crate in the workspace depends on the old signature of prepend_path_entry_for_codex_aliases() (the one without the exe parameter). The git diff shows only codex-rs/arg0/src/lib.rs was changed, but callers in other crates (e.g., codex-core, codex-tui) would need to be updated if they call this function. Mitigation: The diff only touches codex-rs/arg0/ files, and cargo check passed, which means either (a) no other crate calls this function, or (b) the callers are in deleted files (this fork removed the TUI and other crates). In the upstream context, the caller is arg0_dispatch_or_else which is in the same file.
  1. Cross-platform behavior: The Windows batch script change (line 458-464) uses \r\n line endings and quotes the exe path. Could not verify this works correctly on Windows with paths containing spaces. The upstream used \n without \r, so the PR adds \r\n which is more correct for Windows batch files.
  1. fs_err lock API: Verified that fs_err does not provide lock APIs (the try_lock method comes from fs2/fs4 which fs_err::File re-exports). The try_lock_arg0_dir function correctly handles the TryLockError type.

---

Final verdict

✅ Ready to merge — no blocking issues.

The PR achieves its goal: all filesystem errors in codex-arg0 now include the path and operation context. The fs_err migration is clean, the clippy.toml prevents regression, and the custom ContextIoError wrapper correctly preserves the error source chain. The scope is appropriately limited to codex-arg0, with workspace-wide migration deferred to follow-up PRs (as discussed in the issue).

3 nits (non-blocking):

  1. with_path_context doc comment wording could be clearer about with_context usage (Finding 1.1 / 4.1)
  2. No test exercises with_context wrapping a real fs_err error (Finding 5.3)
  3. Privacy note: error messages now include filesystem paths (Finding 7.2 — acceptable per issue discussion)
HaleTom · 2 months ago

Production Readiness Review: enhance-arg0-error-context (2nd pass)

PR: HaleTom/codex#1 (branch: enhance-arg0-error-context)
Reviewed: 2026-05-20T05:59:18+00:00
Scope: Changes since 1st review: 1 file changed, +32 / -3 (doc fix + new test)

Context files read

  • codex-rs/arg0/src/lib.rs:334-339 — updated doc comment
  • codex-rs/arg0/src/lib.rs:782-809 — new test
  • Previous review: REVIEW-2026-05-19T17:32:10+07:00.md — all 3 nits addressed

Verification evidence

  • cargo test -p codex-arg09/9 passed (0.00s) — new test with_context_on_fs_err_error_preserves_path_without_duplication passes
  • cargo clippy -p codex-arg0passed (0 warnings)

---

Production-ready bar for this PR

  1. All std::fs calls in production code replaced with fs_err equivalents — errors must include the file path
  2. Lock-acquire failures include the lock file path and "acquire lock" operation context
  3. ContextIoError preserves the Error::source() chain so anyhow and downstream error walkers see the original OS error
  4. deep_raw_os_error correctly walks the source chain to recover raw_os_error() lost by io::Error::new
  5. clippy::disallowed_methods lint prevents future std::fs regression in this crate
  6. No duplicate path information in error messages — with_context for fs_err calls (path already in message), with_path_context only for non-fs_err sites
  7. Public API change (prepend_path_entry_for_codex_aliases now takes exe: Option<&Path>) is intentional and the single caller is updated
  8. current_exe() hoisted out of the per-filename loop — eliminates redundant syscalls
  9. Tests cover all new helper functions and the source-chain walking behavior
  10. No blocking clippy warnings, no new unsafe code introduced

---

Findings

1. Correctness & functional completeness

Finding 1.1 — with_path_context doc comment clarified (1st review Finding 1.1/4.1 fixed)

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:336-339
  • Confidence: High

The doc now reads: "For errors from fs_err operations (which already include the path), prefer [with_context] to add operation context without duplicating the path in the output." This is clear and unambiguous.

Finding 1.2 — Source chain is correctly preserved

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:287-303 (ContextIoError), codex-rs/arg0/src/lib.rs:299-302 (Error impl)
  • Confidence: High

No changes since 1st review. Source chain preservation verified.

Finding 1.3 — try_lock_arg0_dir error conversion is correct

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:571-580
  • Confidence: High

No changes since 1st review.

Finding 1.4 — current_exe hoisting eliminates redundant syscall

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:435-440
  • Confidence: High

No changes since 1st review.

2. Architecture & boundary integrity

Finding 2.1 — fs_err migration is correctly scoped

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/Cargo.toml:24, codex-rs/arg0/clippy.toml
  • Confidence: High

No changes since 1st review.

Finding 2.2 — Public API change is intentional

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:385-387
  • Confidence: High

No changes since 1st review.

Finding 2.3 — Two lock functions with distinct semantics

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:546-563, codex-rs/arg0/src/lib.rs:565-580
  • Confidence: High

No changes since 1st review.

3. Code clarity, clean code & maintainability

Finding 3.1 — ContextIoError is minimal and correct

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:287-303
  • Confidence: High

No changes since 1st review.

Finding 3.2 — Error context strings are descriptive

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:388-464
  • Confidence: High

No changes since 1st review.

4. Comments & code documentation

Finding 4.1 — with_path_context doc comment (1st review Finding 1.1/4.1 fixed)

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:334-339
  • Confidence: High

Fixed in this commit. The rewording removes ambiguity about with_context usage.

Finding 4.2 — deep_raw_os_error doc comment is clear

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:305-312
  • Confidence: High

No changes since 1st review.

Finding 4.3 — clippy.toml comments explain exceptions

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:2-6
  • Confidence: High

No changes since 1st review.

5. Tests & validation

Finding 5.1 — New tests cover all helper functions

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:713-809
  • Confidence: High

Now 4 new tests (was 3):

  1. with_path_context_includes_operation_path_and_source (line 714)
  2. deep_raw_os_error_retrieves_original_os_code (line 739)
  3. with_context_includes_custom_message_and_source (line 756)
  4. with_context_on_fs_err_error_preserves_path_without_duplication (line 782) — NEW

Finding 5.2 — Existing tests pass unchanged

  • Classification: PASS
  • Type: Verified issue
  • Evidence: cargo test -p codex-arg0 → 9/9 passed
  • Confidence: High

Finding 5.3 — with_context wrapping real fs_err error (1st review Finding 5.3 fixed)

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:782-809
  • Confidence: High

Fixed in this commit. The new test:

  1. Creates a real fs_err error via fs_err::read_to_string("/nonexistent_path_that_does_not_exist_12345")
  2. Wraps it with with_context(err, "read config file")
  3. Verifies the context string appears in the output
  4. Verifies the fs_err path is preserved in the message
  5. Verifies the path appears exactly once (no duplication)
  6. Verifies the source chain preserves the original error

6. Performance

Finding 6.1 — current_exe hoisting saves syscalls

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:435-440
  • Confidence: High

No changes since 1st review.

Finding 6.2 — ContextIoError overhead is on error paths only

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:345-354, codex-rs/arg0/src/lib.rs:361-369
  • Confidence: High

No changes since 1st review.

7. Operational risk

Finding 7.1 — fs_err migration is contained to codex-arg0

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/clippy.toml, codex-rs/arg0/Cargo.toml
  • Confidence: High

No changes since 1st review.

Finding 7.2 — Error messages now include paths (privacy consideration)

  • Classification: NON-BLOCKING
  • Type: Plausible risk
  • Evidence: All .map_err calls in prepend_path_entry_for_codex_aliases
  • Confidence: Medium

No changes since 1st review. Acceptable per issue discussion.

Finding 7.3 — #![deny(clippy::disallowed_methods)] is crate-level

  • Classification: PASS
  • Type: Verified issue
  • Evidence: codex-rs/arg0/src/lib.rs:1
  • Confidence: High

No changes since 1st review.

8. Adversarial review

Adversarial 1 — Could ContextIoError break error downcasting?

Assessment: Low risk. No code in this crate downcasts the returned errors. anyhow walks the source chain.

Adversarial 2 — Could the exe parameter to prepend_path_entry_for_codex_aliases be None when it shouldn't be?

Assessment: Correct behavior. Falls back to current_exe() with context.

Adversarial 3 — Could deep_raw_os_error infinite loop?

Assessment: Not possible. ContextIoError::source() returns a finite chain.

Adversarial 4 — Is the with_context call on find_codex_home() correct?

Assessment: Not an issue. find_codex_home returns io::Result<PathBuf>.

Adversarial 5 — Could the new test break on systems where /nonexistent_path_that_does_not_exist_12345 exists?

Assessment: Not practically possible. The path is highly specific and would require manual creation. Even if it existed, the test would fail with a clear error message, not silently pass.

---

What I could not fully verify

  1. Workspace-wide impact: Same as 1st review — could not verify that no other crate depends on the old signature of prepend_path_entry_for_codex_aliases(). cargo check passed, confirming no breakage in the workspace.
  1. Cross-platform behavior: Same as 1st review — Windows batch script behavior with paths containing spaces is untestable in this environment.

---

Final verdict

✅ Ready to merge — no blocking issues.

All 3 nits from the 1st review have been addressed:

  1. with_path_context doc comment clarified (Finding 1.1/4.1)
  2. with_context wrapping real fs_err error test added (Finding 5.3)
  3. ✅ Privacy note remains non-blocking (Finding 7.2 — no change needed)

The PR is in its final state. All filesystem errors in codex-arg0 include path and operation context, the fs_err migration is clean, the clippy.toml prevents regression, and all helper functions are tested with both synthetic and real errors.

HaleTom · 2 months ago

Workspace-wide impact verification

Question: Does the signature change to prepend_path_entry_for_codex_aliases(exe: Option<&Path>) break any other crate in the workspace?

Answer: Non-issue — zero other callers exist.

  1. Only one .rs file references prepend_path_entry_for_codex_aliases across the entire workspace: codex-rs/arg0/src/lib.rs
  1. Call sites:
  • codex-rs/arg0/src/lib.rs:385 — declaration: pub fn prepend_path_entry_for_codex_aliases(exe: Option<&Path>)
  • codex-rs/arg0/src/lib.rs:150 — only call: match prepend_path_entry_for_codex_aliases(exe.as_deref())
  • codex-rs/arg0/src/lib.rs:196 — comment only
  1. Zero references in codex-core, codex-tui, or any other workspace crate — confirmed via rg -l for the symbol name across all .rs files in codex-rs/.
  1. The pub visibility is cosmetic within the crate. cargo check passing was already sufficient evidence; this just confirms there is no workspace-wide scope for this concern.
HaleTom · 2 months ago

@joshka I think this is now ready to merge -- would you please take a look?

joshka · 2 months ago

Hey - I'm just an external party here. I'll leave it with @etraut-openai to look into this.

Keesan12 · 2 months ago

+1. For sandbox/runtime bugs the missing path is only half the story; the receipt also needs the attempted operation and the effective authority at the time (read-only/workspace/full, sandbox vs host path).

Otherwise Permission denied still leaves people guessing whether the defect is filesystem state, a stale lock, or the policy layer. We ended up treating those as first-class runtime receipts in MartinLoop because diagnosability and auditability become the same problem once retries are automated.

joshka · 2 months ago

@Keesan12 use the thumbs up button - it gives a signal to OpenAI to consider putting effort into fixing an issue.