Bug Report: PATH ordering is mutated when Codex shells launch via bash -lc

Open 💬 10 comments Opened Sep 25, 2025 by jessfraz

Summary

Codex rewrites every shell tool call into bash -lc "<user command>". On macOS (and any system where /etc/profile invokes path_helper), that login shell rebuilds PATH and prepends system entries, so the user’s PATH ordering is lost before their command runs. The mutation breaks toolchains that rely on PATH priority—e.g., Nix-provided cargo shims fall behind Apple’s toolchain, yielding missing libxml2 when Codex replays cargo clippy while the same command succeeds in the user’s terminal.

Impact

  • Nix-managed toolchains, language shims, or custom PATH prefixes stop working inside Codex even though they work locally.
  • PATH order is globally scrambled, so any expectation about “my bin directory wins” fails, independent of macOS.
  • Commands that implicitly depend on PATH (dynamic linker lookup, plugin discovery, wrapper scripts) exhibit inconsistent behavior between Codex and a real terminal.

Environment

  • macOS 14.6.1 (Apple Silicon)
  • Shared Nix shell
  • User login shell: /bin/zsh
  • Codex CLI default shell translation logic (Shell::Bash, use_profile = true)

Steps to Reproduce

  1. In a regular terminal:
export PATH="/tmp/test-bin:/usr/bin"
echo "Before:  $PATH"
/bin/bash -lc 'echo "bash -lc: $PATH"'
bin/bash -c  'echo "bash -c:  $PATH"'
/bin/zsh  -lc 'echo "zsh -lc:  $PATH"'
  1. Observed output on 2025-09-25:
Before:  /tmp/test-bin:/usr/bin
bash -lc: /usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Applications/Little Snitch.app/Contents/Components:/tmp/test-bin
bash -c:  /tmp/test-bin:/usr/bin
zsh -lc:  /tmp/test-bin:/usr/bin
  1. Trigger the same command through Codex’s shell tool; the PATH matches the bash -lc case above.

Observed vs. Expected

  • Observed: Codex’s login bash shell prepends Apple system paths in front of the caller’s PATH, demoting Nix and user prefixes.
  • Expected: Codex should preserve the exact PATH order supplied in ExecParams.env, matching the caller’s environment.

Root Cause

Suggested Fixes

  1. Preferred: Stop forcing login shells by default. Use bash -c unless the user explicitly asked for profiles (e.g., use_profile). That preserves PATH while respecting existing behavior for users who opt in.
  2. Fallback: If login shells must stay, restore the original PATH ordering immediately before executing the user script (e.g., inject export PATH='$ORIGINAL' into the generated command). Risky and brittle.
  3. Regression Test: Add an integration test that runs through the Codex shell path with a sentinel PATH (/tmp/a:/tmp/b), captures echo $PATH inside the spawned process, and asserts exact equality.

Supporting Docs

  • GNU Bash manual (login shells read /etc/profile).
  • macOS path_helper man page (path_helper rebuilds PATH from /etc/paths*).
  • External analysis of path_helper ordering behavior.

View original on GitHub ↗

10 Comments

jessfraz · 9 months ago

also if you tell me what you want to do here, i will do it, but I don't know the history of "why login shells" so i don't feel comfortable scorching them from the earth without knowing if there was a good reason, even tho i can't personally think of one, i don't have all the context.

gcp · 9 months ago
maruel · 9 months ago

Even on linux in a docker container purely using bash, PATH is modified in surprising way:

$ echo $PATH
/home/user/.local/bin:/home/user/.nvm/versions/node/v24.9.0/bin:/usr/local/go/bin:/home/user/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/
local/games:/usr/games

$ codex
> run 'echo $PATH'
 Ran echo $PATH
  └ /home/user/.local/bin:/usr/local/go/bin:/home/user/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

> PATH is /home/user/.local/bin:/usr/local/go/bin:/home/user/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games.

In my .bashrc, I have:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

yet it's not there.

gcp · 9 months ago
In my .bashrc, I have:

A login shell will never read .bashrc

azarenkovgd · 9 months ago

Are there any estimates when this bug will be fixed? It severely affects my workflow and forces to use some messy workarounds.

Maybe there are some official recommendations what to do in this case until the bug is fixed.

ianb · 9 months ago

Note this breaks something as simple as nvm, which modifies the $PATH to set the node interpreter. nvm use also doesn't work within Codex (presumably because environment changes aren't persisted between commands in codex). You can work around this by telling codex to use the absolute path to whatever command or interpreter you want (e.g., (e.g., /Users/username/.nvm/versions/node/v25.0.0/bin/npm), but it's awkward and codex tends to forget to do this.

nachoal · 8 months ago

This is such a basic thing to get right, is there some context on why you use login shells? Extremely disruptive to workflow, part of the reason why claude code is amazing at tooling an cli work (they get bash execution right)

imajes · 7 months ago

Thanks for writing this up @jessfraz -- really does feel like there's not a clear sense of whether or not a user's shell + env is particularly respected.

@etraut-openai: I promise I won't keep tagging you in, but you have been doing Sisyphean work in keeping up with PRs/issues. This one matters though. it seems impossible now to say, "i use zsh, please don't create a new shell every time to run your commands?"

On top of that- it's now cd ... into the folder it's already in every time. is it not keeping track of cwd ?

JaysonGeng · 3 months ago

I dug through the current code path and put together a targeted fix on my fork:

Ready branch:
https://github.com/JaysonGeng/codex/tree/fix/shell-path-order-preservation

Compare:
https://github.com/JaysonGeng/codex/compare/main...fix/shell-path-order-preservation

Commit:
https://github.com/JaysonGeng/codex/commit/c5524e5

What it changes:

  • default shell_command / exec_command to non-login shells when login is omitted
  • keep allow_login_shell = true as an explicit opt-in gate for login = true
  • stop /shell (user_shell) from hardcoding login-shell execution
  • keep memories/usage aligned with the new command shape
  • update tool/config schema text so model guidance prefers bash -c over bash -lc

Validation:

  • cargo test -p codex-tools passed
  • just fix -p codex-core passed
  • just fix -p codex-tools passed
  • just fmt passed
  • cargo test/check -p codex-core under a 60s hard-timeout repeatedly reached the codex-core test/check target without surfacing compile or test failures, but did not fully complete within the timeout on this machine
  • just argument-comment-lint could not run here because the local environment is missing Bazel and tools/argument-comment-lint/*

If maintainers are interested, I can rebase or split this further, but this should already be reviewable as-is.

JaysonGeng · 3 months ago

Per the current contributing policy in docs/contributing.md, I am intentionally not opening an unsolicited upstream PR for this.

The branch and commit are already in a maintainer-ready state:

If this approach matches the intended direction, I can open the PR immediately once a maintainer invites it; otherwise feel free to cherry-pick c5524e5 directly or ask me to rebase/split it further.