Skip to content

codex-account wrapper and host keyring guard disagree on CODEX_HOME #441

Description

@hbrodin

CODEX_HOME diverges between the codex-account wrapper and the host-side keyring guard, which can silently degrade ChatGPT auth to a plaintext token.

Follow-up from the review of #438, filed rather than fixed there to keep that PR to one logical change.

The divergence

The guest wrapper resolves the config Codex will actually read:

# scripts/guest/codex-account.sh
CODEX_CONFIG="${CODEX_HOME:-$HOME/.codex}/config.toml"

The two host-side halves added in #438 hardcode ~/.codex:

// src/backend.rs — ensure_codex_keyring_configured
"grep -Eq '^[[:space:]]*cli_auth_credentials_store[[:space:]]*=[[:space:]]*\"keyring\"' ~/.codex/config.toml"

// src/backend.rs — remove_guest_codex_auth_json
.literal("rm -f ~/.codex/auth.json")

With CODEX_HOME set for the guest session and pointing at a config that lacks the key, the host guard passes against the bootstrap-written ~/.codex/config.toml while the wrapper reads the other file, finds no keyring key, and execs plain Codex. coop codex -- login then writes a plaintext refresh token to $CODEX_HOME/auth.json, which remove_guest_codex_auth_json never cleans. If that path is under /workspace, the token also rides the workspace pull back to the host.

This falsifies a guarantee docs/trust-model.md currently asserts:

coop codex refuses to launch when the guest config does not actually select the keyring store

Reachability

CODEX_HOME is a valid EnvVarName (src/guest_env_state.rs), so guest_env, codex.env_forward, and coop start --env all forward it, and both backends append AcceptEnv * (scripts/guest/guest-config.sh, src/lima.rs). It reaches the interactive coop codex session via open_ssh_sessionprepare_session_from_targetprepare_env_forwardingSendEnv. #438's own integration test uses exactly this lever:

coop_exec env CODEX_HOME=/tmp/coop-keyring-probe /usr/local/bin/codex-account --version

Why the obvious fix does not work

Changing the host grep to ${CODEX_HOME:-$HOME/.codex} does not close this. Guest env is delivered per-session via SendEnv, not persisted to the guest filesystem, and exec_ok — which ensure_codex_keyring_configured uses — builds a bare ssh invocation with no session env:

pub fn exec_ok(&self, command: RemoteCommand) -> bool {
    let mut args = self.ssh_opts();
    args.push(self.addr());
    args.push(command.into_string());
    Command::new("ssh").args(&args)   // no .envs(...), no SendEnv

while ssh::run_interactive does .envs(session.env.as_envs()) plus -o SendEnv=. The guard would keep expanding to ~/.codex while the interactive session expands to $CODEX_HOME.

Suggested fix

Note that coop does not support CODEX_HOME anywhere: all Codex staging hardcodes .codex (copy_staged_to_guest(target, &staged, ".codex", "Codex"), the plugin-state round-trip). Setting it already breaks MCP servers, plugins, and model routing on main. So the goal is not to plumb CODEX_HOME through one path — full support would be its own change — but to stop an unsupported configuration from degrading into a plaintext credential.

Fail closed in the wrapper's passthrough branch instead:

if ! keyring_mode; then
    # coop stages into $HOME/.codex and cannot honor an arbitrary CODEX_HOME.
    # If coop's own config selects the keyring store but the config Codex will
    # actually read does not, `codex login` would write a plaintext auth.json —
    # the one thing this mode exists to prevent. Refuse instead of degrading.
    if [ "$CODEX_CONFIG" != "$HOME/.codex/config.toml" ] \
        && keyring_mode_in "$HOME/.codex/config.toml"; then
        die "CODEX_HOME is set, but coop manages ~/.codex and it selects the keyring credential store; unset CODEX_HOME for Codex ChatGPT account auth"
    fi
    exec "$CODEX_BIN" "$@"
fi

This needs no host change (sidestepping the exec_ok env problem), fires only in the dangerous case, and is a provable no-op when CODEX_HOME is unset since both paths are then the same file. It also leaves #438's integration test passing: that test points CODEX_HOME at a scratch dir containing the key, so keyring_mode is true and the branch is never reached.

Worth factoring the regex into a keyring_mode_in <path> helper at the same time — it is currently hand-duplicated between the wrapper and ensure_codex_keyring_configured, with nothing pinning the two together.

Also worth doing

  • Add a src/guest.rs assertion for the new guard. Nothing currently tests the wrapper's gate or its env-filter loop.
  • Either implement this or soften the docs/trust-model.md sentence quoted above, so the documented guarantee matches the code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions