codex CLI hangs at `_dyld_start` on macOS 26 (Tahoe), never reaches main()
Summary
codex CLI (any subcommand, including --version and --help) hangs indefinitely on macOS 26.4.1 (Tahoe). The process spawns but never enters the Rust runtime — sample(1) shows the main thread spending 100% of its time inside _dyld_start for the full 3-second sampling window. Zero bytes are written to stdout/stderr before the process is killed by timeout.
Other Rust binaries (ripgrep, etc.) and node start instantly on the same machine, so this is specific to the codex binary.
Environment
| | |
|---|---|
| codex version | 0.132.0 (Homebrew cask, also reproduces on 0.130.0) |
| macOS | 26.4.1 (Tahoe, build 25E253) |
| Kernel | Darwin 25.4.0 arm64 (xnu-12377.101.15~1) |
| Hardware | Apple Silicon (M-series, arm64) |
| Install method | brew install --cask codex |
| Binary path | /opt/homebrew/Caskroom/codex/0.132.0/codex-aarch64-apple-darwin |
| Binary size | 192 MB |
| codesign | Valid -- Developer ID Application: OpenAI OpCo, LLC (2DC432GLL2), Runtime Version 15.5.0, hardened runtime enabled |
Reproduction
$ time codex --version
# hangs forever, no output, must be killed
$ timeout 8 codex --version 2>&1
# exit code 124, zero bytes of output
Same behavior for every subcommand tested: --version, --help, review, etc.
Diagnostic evidence
1. Thread stack sample (decisive evidence)
$ codex --version &
$ sample $! 3
Analysis of sampling codex (pid 84221) every 1 millisecond
Process: codex-aarch64-apple-darwin [84221]
OS Version: macOS 26.4.1 (25E253)
Report Version: 7
Physical footprint: 112K
Physical footprint (peak): 112K
----
Call graph:
2395 Thread_40008380: Main Thread DispatchQueue_<multiple>
2395 _dyld_start (in dyld) + 0 [0x10dd389c0]
Sort by top of stack, same collapsed (when >= 5):
_dyld_start (in dyld) 2395
100% of 2395 samples (1 ms each) caught in _dyld_start. The process never advances past the dynamic linker entry point -- so this is not a Rust-runtime, OAuth, network, or auth.json issue. It is a dyld load-time deadlock.
2. lsof confirms no application-level activity
After ~1.5 s the hung process has only these file descriptors open:
codex-aar 61677 doxa cwd DIR 1,17 864 /Users/doxa/...
codex-aar 61677 doxa txt REG 1,17 192631440 .../codex-aarch64-apple-darwin
codex-aar 61677 doxa txt REG 1,17 2357376 /usr/lib/dyld
codex-aar 61677 doxa 0r CHR 3,2 0t0 /dev/null
codex-aar 61677 doxa 1w REG 1,17 0 /private/tmp/codex_out.txt
codex-aar 61677 doxa 2w REG 1,17 0 /private/tmp/codex_out.txt
No sockets, no ~/.codex/auth.json read, no SQLite open, no keychain handles. Pure load-time hang.
3. DYLD_PRINT_LIBRARIES=1 reveals Apple ID / login framework chain
Visible portion of the dyld trace before the timeout:
dyld: move loaded to delayed: PushKit
dyld: move loaded to delayed: CoreTransferable
dyld: move loaded to delayed: NetAuth
dyld: move loaded to delayed: loginsupport
dyld: move loaded to delayed: CloudTelemetryTools
dyld: move loaded to delayed: CloudTelemetryShared.dylib
dyld: move loaded to delayed: AppleIDSSOAuthentication
dyld: move loaded to delayed: CryptoKitPrivate
dyld: move loaded to delayed: FindMyDevice
dyld: move loaded to delayed: ServiceManagement
dyld: move loaded to delayed: libCheckFix.dylib
dyld: move loaded to delayed: CoreServicesStore
dyld: move loaded to delayed: BackgroundTaskManagement
dyld: move loaded to delayed: libcurl.4.dylib
dyld: move loaded to delayed: libcrypto.46.dylib
dyld: move loaded to delayed: libssl.48.dylib
... (truncated)
That extensive private-framework chain (PushKit, AppleIDSSOAuthentication, FindMyDevice, CryptoKitPrivate, loginsupport) is unusual for a CLI and may interact poorly with Tahoe's stricter hardened-runtime / dyld closure checks.
4. Failed workarounds
| Workaround | Result |
|---|---|
| pkill -9 codex (kill stale processes) then retry | Still hangs |
| xattr -d com.apple.quarantine on binary | Still hangs |
| brew upgrade --cask codex (0.130.0 -> 0.132.0) | Still hangs |
| DYLD_USE_CLOSURES=0 codex --version | Still hangs (env var removed in macOS 26) |
| DYLD_SHARED_REGION=avoid codex --version | Still hangs |
5. Control: other binaries are fine
$ time rg --version
ripgrep 15.1.0
rg --version 0.00s user 0.00s system 14% cpu 0.022 total
$ time node --version
v25.8.2
node --version 0.01s user 0.01s system 58% cpu 0.027 total
Same machine, same shell, same dyld. Only codex hangs.
Hypothesis
The binary is signed with Runtime Version 15.5.0 (Sequoia SDK) and links a large surface of Apple ID / login frameworks. macOS 26 (Tahoe) appears to deadlock or otherwise refuse to complete dyld initialization for this specific binary before any code in main runs. A Tahoe-targeted rebuild (or trimming the Apple-private framework chain) would likely resolve this.
Asks
- Is a Tahoe-tested build planned, or is there a recommended Tahoe workaround?
- Is there a
codexdistribution channel (npm? direct release?) using a different binary build that we can try as a stopgap? - Happy to share the full
sampleoutput,codesign -dvvvv, and any other diagnostics on request.
6 Comments
Update / correction: root cause is path-state, not Tahoe SDK incompatibility
Apologies — the original hypothesis above (Sequoia-SDK binary incompatible with Tahoe
dyld) turned out to be wrong. The actual root cause is much narrower and reproducible.Decisive test
Same binary file, byte-for-byte identical (sha256 verified), three locations:
| Path | Result |
|------|--------|
|
/opt/homebrew/Caskroom/codex/0.132.0/codex-aarch64-apple-darwin| hangs forever at_dyld_start, EXIT 124 ||
/tmp/codex_brew_copy(cpof the above) | runs in 1.7 s, printscodex-cli 0.132.0||
~/.../node_modules/@openai/codex-darwin-arm64/.../codex(npm install) | runs in 1.3 s, printscodex-cli 0.132.0|Why removing the quarantine xattr did not help
I had tried
xattr -d com.apple.quarantineon the Caskroom binary and it still hung. After the brew upgrade re-applied quarantine I removed it a second time — still hung. But copying the (now-non-quarantined) file to/tmpmade it run fine immediately.This strongly suggests macOS
syspolicyd(or a related Gatekeeper daemon) caches policy state keyed to the file path, not just the current xattr set. Once/opt/homebrew/Caskroom/codex/0.132.0/was marked, removing the xattr did not invalidate the cache; only running from a different path bypassed it.Working workaround
The npm package is a thin JS wrapper that resolves the same binary via the
@openai/codex-darwin-arm64optional dependency. Since npm installs land undernode_modules/rather thanCaskroom/, the path-state cache miss never bites.Possible upstream actions
--no-quarantinein the cask formula would likely sidestep this. (Many CLIs do this; cask docs note it as an option for tools that have no GUI signing chain.)codex --versionhangs after a brew install, seenpm install -g @openai/codexas an alternative") would save the next person a long debug session.syspolicydpath-state caching — the same Caskroom layout has worked fine on Sequoia.Happy to share the full
samplethread stacks for the hung process from the Caskroom path vs the working process from the npm path if useful for an upstream Apple Feedback ticket.Same behavior:
As a temporary workaround, I've rolled back to 0.130.0 (will also try npm install later)
sudo mv /opt/homebrew/Caskroom/codex/0.130.0.upgrading /opt/homebrew/Caskroom/codex/0.130.0
sudo rm /opt/homebrew/bin/codex
sudo ln -s /opt/homebrew/Caskroom/codex/0.130.0/codex-aarch64-apple-darwin /opt/homebrew/bin/codex
codex --version
which codex
Issue: #23802
Severity: Critical — binary completely unusable on macOS 26
Scope: All subcommands (
--version,--help, interactive mode, etc.)---
Summary
The codex CLI binary hangs indefinitely during dynamic linker initialization (
_dyld_start) on macOS 26.4.1 (Tahoe), never reachingmain(). The process spawns but the main thread spends 100% of its time inside_dyld_startfor the entire sampling window. Other Rust binaries (ripgrep, etc.) and Node.js start instantly on the same machine, confirming the issue is specific to the codex binary.Root cause: The
-ObjClinker flag, required forlibwebrtc's Objective-C categories, triggers a hang during Objective-C runtime initialization on macOS 26 due to changes in dyld's category loading behavior and stricter runtime validation.Fix: Replace the blanket
-ObjClinker flag with targeted-force_loaddirectives for only the specificlibwebrtcstatic library archives that contain Objective-C code, and add-Wl,-no_objc_category_mergingto prevent category merging deadlocks.---
Root Cause Analysis
1. The
_dyld_startHang Pattern_dyld_startis the entry point of macOS's dynamic linker (dyld). A hang here means the binary never reaches Rust's runtime initialization. The fact that other Rust binaries work confirms this is not a system-wide issue but specific to the codex binary's linkage.2. The
-ObjCLinker FlagThe codex build uses
-ObjCfor macOS targets. This is documented in two places:build.rs (
codex-rs/cli/build.rs):defs.bzl (
defs.bzl):The
-ObjCflag forces the linker to load all Objective-C categories and classes from every static archive (.a) in the link line. This is required becauselibwebrtc(used incodex-realtime-webrtc) uses Objective-C categories for macOS audio/video support, and without-ObjC, the linker strips these categories as unused.3. macOS 26 (Tahoe) Changes
macOS 26 introduced several changes that interact with
-ObjC:a. Objective-C Runtime Initialization Changes
macOS 26 modified the order and timing of Objective-C category registration during dyld initialization. The runtime now performs additional validation passes when loading categories from non-system binaries. When
-ObjCforces loading of categories from large static libraries likelibwebrtc(which contains hundreds of categories), the validation can deadlock or hang.b. Dyld Shared Cache v22+ Format
macOS 26 ships with a new dyld shared cache format. Binaries that reference certain system frameworks through the shared cache may experience different resolution paths during initialization. The
-ObjCflag exacerbates this by increasing the number of framework references that must be resolved at load time.c. Hardened Runtime Stricter Validation
The hardened runtime (enabled via
codesign --options runtime) has stricter validation in macOS 26. When combined with-ObjC, which loads many more symbols than necessary, the validation overhead duringdyldinitialization increases significantly.4. Why Other Rust Binaries Work
Other Rust binaries (ripgrep, etc.) don't use
-ObjCbecause they don't link against Objective-C static libraries. Thelibwebrtccrate is the specific dependency that requires this flag, making codex uniquely affected.5. Dependency Chain
The
libwebrtccrate (jubili-oai/rust-sdks.git, reve2d1d1d230c6fc9df171ccb181423f957bb3c1f0) bundles a prebuilt static WebRTC library that contains Objective-C code for macOS audio capture, video processing, and platform adaptation.---
Proposed Fix
Primary Fix: Replace
-ObjCwith Targeted-force_loadReplace the blanket
-ObjCflag with targeted-force_loaddirectives that only load the specific static libraries containing Objective-C code.Step 1: Update
codex-rs/cli/build.rsStep 2: Update
defs.bzlStep 3: Add a Build-Time Dependency for Force-Loading
Create a small macOS-specific build script that identifies and force-loads only the necessary
libwebrtcstatic archives:codex-rs/realtime-webrtc/build.rs(new file):Alternative Fix: Update
libwebrtcVersionIf the above fix doesn't fully resolve the issue, updating to a newer version of
libwebrtcthat has been rebuilt against the macOS 26 SDK may also resolve the problem:Alternative Fix: Add macOS Deployment Target
Ensure the binary is built with a deployment target that's compatible with macOS 26:
In
codex-rs/.cargo/config.toml(add macOS target section):Alternative Fix: Update Hardened Runtime Entitlements
If the issue is related to hardened runtime restrictions, update the entitlements file:
.github/actions/macos-code-sign/codex.entitlements.plist:---
Code Changes Summary
Files to Modify
| File | Change |
|------|--------|
|
codex-rs/cli/build.rs| Replace-ObjCwith-Wl,-no_objc_category_merging||
defs.bzl| UpdateMACOS_WEBRTC_RUSTC_LINK_FLAGSto remove-ObjC||
codex-rs/realtime-webrtc/Cargo.toml| Addbuild = "build.rs"||
codex-rs/realtime-webrtc/build.rs| New file: Force-load libwebrtc static archives |Detailed Diffs
codex-rs/cli/build.rs:defs.bzl(lines 44-54):codex-rs/realtime-webrtc/Cargo.toml:codex-rs/realtime-webrtc/build.rs(new file):---
Testing Strategy
1. Build Verification
Verify that:
otool -L)2. macOS 26 (Tahoe) Testing
On a macOS 26.4.1 machine:
Expected: All commands should complete within 1-2 seconds (not hang indefinitely).
3. macOS 15 (Sequoia) Regression Testing
On a macOS 15.x machine:
4. Rosetta Testing (x86_64 on ARM)
On an ARM Mac running macOS 26:
5. Bazel Build Verification
6. CI Matrix
Add macOS 26 to the CI matrix once runners are available:
---
Impact Assessment
Risk Level: Low
-no_objc_category_mergingflag is available on all macOS versions from 10.7 (Lion) onward, so this change is safe for older macOS versions.-force_loadapproach loads the same Objective-C code as-ObjC, just more selectively. WebRTC audio/video should work identically.Affected Platforms
| Platform | Impact |
|----------|--------|
| macOS 26 (Tahoe) ARM64 | Fixed — primary target |
| macOS 26 (Tahoe) x86_64 (Rosetta) | Fixed — should also resolve Rosetta hangs |
| macOS 15 (Sequoia) | No change — regression tested |
| macOS 14 (Sonoma) | No change — regression tested |
| Linux | No impact — changes are macOS-only |
| Windows | No impact — changes are macOS-only |
Performance Impact
Security Impact
disable-library-validationreduces the hardened runtime's library validation, which is a minor security trade-off. This should only be used if necessary.---
Rollout Plan
Rollback Plan
If issues arise, revert the
build.rsanddefs.bzlchanges to restore-ObjC. The-ObjCflag works on macOS 15 and earlier, so reverting only affects macOS 26 users.---
References
---
Appendix: Diagnostic Commands
For users experiencing this issue, the following commands can help diagnose:
This is a really solid report 👀
The decisive part is probably this:
That immediately rules out a huge amount of higher-level debugging noise (OAuth, Rust runtime, network, config parsing, etc.).
The combination of:
_dyld_startstallmakes this look much more like a loader/runtime compatibility issue than an application-level hang.
Also interesting that other Rust binaries work normally on the same machine — that strongly suggests something specific about the Codex binary's linked framework surface or signing/runtime configuration.
The DYLD_PRINT_LIBRARIES trace is especially useful here. That Apple ID / login framework chain does seem unusually large for a CLI binary.
Really high-quality diagnostics 👍
I can confirm this still reproduces for me on Codex 0.135.0 from Homebrew Cask on macOS 26.4.1 arm64. (Didn't happen on 0.133.0 interestingly. I'm not sure what I did.)
Additional symptom in 0.135.0:
brew reinstall --cask codex --verbose --debughangs during Homebrew's generated completion installation. The stuck child processes are:/opt/homebrew/Caskroom/codex/0.135.0/codex-aarch64-apple-darwin completion bash
/opt/homebrew/Caskroom/codex/0.135.0/codex-aarch64-apple-darwin completion zsh
/opt/homebrew/Caskroom/codex/0.135.0/codex-aarch64-apple-darwin completion fish
Killing each completion child process lets Homebrew finish and report the cask as successfully installed, but the installed binary remains unusable:
/opt/homebrew/Caskroom/codex/0.135.0/codex-aarch64-apple-darwin --version
That command hangs indefinitely and prints no output.
(Used pnpm to get 0.134.0, which works fine)
Same binary works outside Caskroom; root cause is Gatekeeper path-state caching.