Bug Report: PATH ordering is mutated when Codex shells launch via bash -lc
Summary
Codex rewrites every shell tool call into bash -lc "
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
- 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"'
- 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
- 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
- Shell::format_default_shell_invocation always produces [shell_path, "-lc",
- maybe_translate_shell_command swaps in that wrapper whenever ShellEnvironmentPolicy.use_profile is true (default on macOS). (core/src/codex.rs) core/src/codex.rs#L2602-L2687 (https://github.com/openai/codex/blob/e85742635f8e958c22ad46b5428f16b973c6b38d/codex-rs/core/src/codex.rs#L2602-L2687)
- The child process is spawned with a clean environment that Codex repopulates, so PATH enters exactly as provided before bash login hooks run. (core/src/spawn.rs, core/src/exec_env.rs) core/src/spawn.rs#L38-L107 (https://github.com/openai/codex/blob/e85742635f8e958c22ad46b5428f16b973c6b38d/codex-rs/core/src/spawn.rs#L38-L107) core/src/exec_env.rs#L14-L68 (https://github.com/openai/codex/blob/e85742635f8e958c22ad46b5428f16b973c6b38d/codex-rs/core/src/exec_env.rs#L14-L68)
- GNU Bash treats -l / --login as a login shell and executes /etc/profile before the user command. GNU Bash manual (https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html)
- On macOS, /etc/profile runs /usr/libexec/path_helper -s, which rebuilds PATH from /etc/paths and /etc/paths.d, then appends the previous PATH value—earlier entries get pushed behind the system defaults. path_helper man page (https://www.unix.com/man-page/osx/8/path_helper/) path_helper behavior write-up (https://scriptingosx.com/2017/05/path_helper-and-launchctl-setenv/)
Suggested Fixes
- 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.
- 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.
- 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.
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.
Similar to https://github.com/openai/codex/issues/2542.
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.
In my .bashrc, I have:
A login shell will never read .bashrc
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.
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.
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)
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 ?