Skip to content

Regression in Homebrew Codex CLI 0.144.1: GPT-5.6 Code Mode fails with code-mode host exited during handshake, while ChatGPT bundled CLI works #32696

Open

Description

@skyforevers

What version of Codex CLI is running?

0.144.1

What subscription do you have?

plus

Which model were you using?

GPT-5.6 (sol / terra / luna)

What platform is your computer?

Darwin 25.5.0 arm64 arm

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

cmux, iterm2, vscode all

Codex doctor report

What issue are you seeing?

The Homebrew Codex CLI consistently fails to execute Code Mode tool calls with GPT-5.6 models.

Example:

codex exec -m gpt-5.6-terra "pwd"

The tool invocation fails with:


ERROR codex_core::tools::router:

error=code-mode host exited during handshake

The model then answers using the provided environment information instead of actually executing the shell command.


What steps can reproduce the bug?

  1. Install Codex CLI 0.144.1 via Homebrew.

  2. Login with a ChatGPT account.

  3. Run:

codex exec -m gpt-5.6-terra "Run pwd using the shell tool."

or start an interactive session:

codex -m gpt-5.6-terra

and ask:


pwd

What is the expected behavior?

Expected behavior

The shell tool should execute successfully.

Example output:


exec

/bin/zsh -lc pwd

Succeeded

/Users/...


Actual behavior

The shell tool never executes.


ERROR codex_core::tools::router:

code-mode host exited during handshake


Additional information

I spent quite a while isolating this.

The following all work correctly:

  • ChatGPT macOS bundled Codex CLI (0.144.0-alpha.4)

  • GPT-5.5

  • ChatGPT App (same account, same machine)

The following consistently fails:

  • Homebrew Codex CLI 0.144.1

  • GPT-5.6 Terra Code Mode

  • GPT-5.6 Sol Code Mode (tool invocation)

The most convincing reproduction is that the exact same machine, account, repository and prompt work correctly when using the Codex binary bundled inside the ChatGPT macOS app, but fail when using the Homebrew CLI.

Working:

"/Applications/ChatGPT.app/Contents/Resources/codex" exec \

  -m gpt-5.6-terra \

  "Run pwd using the shell tool."

Output:


exec

/bin/zsh -lc pwd

Succeeded

Failing:

codex exec \

  -m gpt-5.6-terra \

  "Run pwd using the shell tool."

Output:


ERROR codex_core::tools::router:

error=code-mode host exited during handshake


Diagnostics

codex doctor reports 0 failures.

Environment is healthy:

  • Auth OK

  • WebSocket OK

  • Sandbox OK

  • Homebrew install consistent

  • No doctor warnings

This looks like a regression specific to the Homebrew 0.144.1 CLI rather than an account or environment issue.

Activity

  1. added
    CLIIssues related to the Codex CLI
    execIssues related to the `codex exec` subcommand
    tool-callsIssues related to tool calling
    on Jul 13, 2026
  2. github-actions commented on Jul 13, 2026

    @github-actions
    Contributor
  3. bbingz commented on Jul 25, 2026

    @bbingz

    Analysis (community)

    Residual analysis for #32696 (Homebrew 0.144.1: code-mode host exited during handshake while the ChatGPT-bundled CLI works) under docs/contributing.md — invitation-only. No unsolicited PR.

    Root-cause hypothesis

    Your A/B result (same version, Homebrew build fails, bundled build works) has a precise OSS-side structural explanation: the code-mode host resolver treats the CLI binary's own invocation path as the source of truth for locating the host binary, and the handshake-failure class bypasses the one safety net that was added for the missing-binary class. Verified at HEAD 4c43465133:

    1. The host path is resolved as a sibling of an un-canonicalized current_exe(), latched once, with no validation. default_host_program() (codex-rs/code-mode/src/remote_session.rs:572-577) calls resolve_host_program(var_os(CODEX_CODE_MODE_HOST_PATH), current_exe()); the resolver (:579-597) returns the env override verbatim if set (no exists() check, no canonicalize), otherwise current_exe().parent().join("codex-code-mode-host") — and there is no .canonicalize() anywhere in the code-mode crate (grep = 0 hits). On macOS, std::env::current_exe() returns the symlink path, not the link target (Linux is immune: /proc/self/exe is already resolved — this is consistent with the Darwin-only host-symlink logic the installer grew in the 0.144.1 packaging fix). The result is then latched once in ProcessOwnedCodeModeSessionProvider::default() (remote_session.rs:81-85), so a stale path survives for the process lifetime even if you repair the install mid-session. A channel-specific path divergence (Homebrew formula layout vs. ChatGPT bundle layout) therefore selects a different — possibly stale or mismatched — host binary per channel, which is exactly your A/B.

    2. The 0.144.1 in-process fallback covers only Spawn + NotFound — handshake failures fall straight through. Commit 8cf9a1b1f8 (#31899) added the degrade-to-embedded-V8 net at remote_session.rs:99-113, gated by error.host_program_not_found() (code-mode/src/remote_session/connection.rs:74-81), which matches only ConnectionError::Spawn with io::ErrorKind::NotFound. Your error string — "code-mode host exited during handshake" — is produced at connection.rs:291 and wrapped as ConnectionError::Other (:304-308; handshake timeout takes the same Other path). Other bypasses the NotFound check and propagates via the bare Err(error) => return Err(error.to_string()) arm (remote_session.rs:113), so when the host binary exists but exits during the V1 handshake (version/channel mismatch between the resolved host and the CLI — precisely what a stale sibling resolution produces), there is no degradation path: every code-mode tool call fails wholesale, and the model falls back to describing the environment instead of executing — matching your report verbatim.

    3. Two amplifier gaps make this invisible until it breaks. (a) codex doctor never validates the host binary's existence/executability, so it reports the install consistent while the resolved host path is wrong (also reported on #31831). (b) The flip to the in-process fallback (remote_session.rs:99-113) emits zero user-visible notice, so the NotFound variant degrades silently while the handshake variant fails loudly — two failure classes with opposite visibility and no telemetry on either.

    What this analysis deliberately does NOT cover: why the Homebrew 0.144.1 host binary exits during handshake while the bundled one does not is packaging-level (channel co-location/signing/layout) and cannot be pinned from this repo — the OSS anchor is the error surface and the missing fallback, not the binary divergence itself. Adjacent spawn-family reports: #31831 (missing host, 0.144.0 — the NotFound class; the packaging half was fixed in 0.144.1, the resolver gaps above remain), #32495 (stale ~/.local/bin/codex symlink after the Codex.app → ChatGPT.app migration re-triggering sibling resolution against a removed bundle). The code-mode token-billing axis (the wait tool's unclamped yield_time_ms) is a different mechanism on the same subsystem, analyzed separately on #32309.

    Fix outline (all OSS, codex-rs/code-mode/): (a) canonicalize current_exe() before parent().join(...) in resolve_host_program and add an exists() precheck with an ordered fallback chain (env override → canonicalized sibling → raw sibling → bare-name PATH lookup), warning when an env override names a nonexistent file; (b) resolve lazily in create_session (or invalidate on failure) instead of latching in Default, so a repaired install takes effect without a restart; (c) extend the fallback classifier beyond Spawn+NotFound to handshake ConnectionError::Other — a present-but-incompatible host should degrade to the embedded V8 session exactly like an absent one, or at minimum enrich the error with a remediation hint ("host/CLI channel mismatch; reinstall or point CODEX_CODE_MODE_HOST_PATH at the matching binary"); (d) emit a one-time user-visible notice on any fallback flip and add a host-binary existence check to codex doctor.

    Fail-first tests I'd want before any merge: (1) invoke via a symlink whose directory lacks the host binary → resolver canonicalizes and finds the real sibling (today: resolves against the symlink's parent); (2) host binary present but written to exit immediately → session degrades to in-process or returns an actionable error (today: bare ConnectionError::Other propagates); (3) a stale env override plus a valid sibling → warning + sibling wins; (4) repair-after-start (host binary appears mid-session) → next create_session succeeds without a process restart (today: latched path sticks). Happy to draft the test patch if a maintainer wants it. No unsolicited PR from me.

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

    CLIIssues related to the Codex CLIbugSomething isn't workingexecIssues related to the `codex exec` subcommandtool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions