You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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?
Install Codex CLI 0.144.1 via Homebrew.
Login with a ChatGPT account.
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.
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-canonicalizedcurrent_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 onlySpawn + 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 onlyConnectionError::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.
Activity
github-actions commentedon Jul 13, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
codex-code-mode-hostevery command is failing with "failed to spawn code-mode host" #31906Powered by Codex Action
bbingz commentedon Jul 25, 2026
Analysis (community)
Residual analysis for #32696 (Homebrew 0.144.1:
code-mode host exited during handshakewhile 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) callsresolve_host_program(var_os(CODEX_CODE_MODE_HOST_PATH), current_exe()); the resolver (:579-597) returns the env override verbatim if set (noexists()check, no canonicalize), otherwisecurrent_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/exeis 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 inProcessOwnedCodeModeSessionProvider::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. Commit8cf9a1b1f8(#31899) added the degrade-to-embedded-V8 net atremote_session.rs:99-113, gated byerror.host_program_not_found()(code-mode/src/remote_session/connection.rs:74-81), which matches onlyConnectionError::Spawnwithio::ErrorKind::NotFound. Your error string —"code-mode host exited during handshake"— is produced atconnection.rs:291and wrapped asConnectionError::Other(:304-308; handshake timeout takes the sameOtherpath).Otherbypasses the NotFound check and propagates via the bareErr(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 doctornever 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/codexsymlink after the Codex.app → ChatGPT.app migration re-triggering sibling resolution against a removed bundle). The code-mode token-billing axis (thewaittool's unclampedyield_time_ms) is a different mechanism on the same subsystem, analyzed separately on #32309.Fix outline (all OSS,
codex-rs/code-mode/): (a) canonicalizecurrent_exe()beforeparent().join(...)inresolve_host_programand add anexists()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 increate_session(or invalidate on failure) instead of latching inDefault, so a repaired install takes effect without a restart; (c) extend the fallback classifier beyondSpawn+NotFoundto handshakeConnectionError::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 pointCODEX_CODE_MODE_HOST_PATHat the matching binary"); (d) emit a one-time user-visible notice on any fallback flip and add a host-binary existence check tocodex 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::Otherpropagates); (3) a stale env override plus a valid sibling → warning + sibling wins; (4) repair-after-start (host binary appears mid-session) → nextcreate_sessionsucceeds without a process restart (today: latched path sticks). Happy to draft the test patch if a maintainer wants it. No unsolicited PR from me.