The trigger is a symlinked codex home. The spawn loop needs both an uncapped retry loop in the app and
~/.codex(orCODEX_HOME) reached through a symlink. See openai/codex#39732, independently replicated here on a known-bad build: symlinked → 73 helpers in 27s; resolved real path → 1 helper, flat for 903s. Same binary, same machine, one variable.Correction 1: the "watch the Computer Use runtime version" signal proposed below is wrong. The runtime moved to
26.817.1000761and builds still stormed — the defect is in the Electron main process, not the helper.Correction 2: downgrading is no longer the recommended fix. Canonicalizing the codex home path fixes it on current builds. This machine now runs 26.818.22352 with Computer History enabled — 1 helper, flat, zero crashes.
Full write-up of the investigation, including every wrong turn: https://gist.github.com/galligan/c8a64ec15b89e11ac0421b1b117056c0
The measurements below remain accurate as recorded; treat the recommendations as superseded.
Independent investigation across two Macs, 2026-08-14 → 2026-08-16. Written for the ChatGPT/Codex desktop team as a record of what we observed and what we tried, with the measurements behind each claim.
Upstream thread: openai/codex#38455
Everything below was measured on our own machines unless explicitly attributed to another reporter. Where we are inferring rather than measuring, we say so.
On 26.810.x, the app spawns SkyComputerUseService helper processes in an unbounded retry
loop from launch, while idle and without Computer Use being invoked. Each retry also creates a
Node worker thread and a V8 isolate inside the main app process. At roughly 600 concurrent
helpers — about 4 minutes after launch — the app fatally aborts while creating another
isolate.
The behavior is independent of user configuration. We were unable to stop it from outside the app binary; the only state in which we could keep working was on a build predating the regression.
| Machine A | Machine B | |
|---|---|---|
| Model | Mac Studio, Apple Silicon | Mac mini, Apple Silicon |
| macOS | 26.6 (25G70), later 27.0 beta (26A5406e) | 27.0 beta (26A5406e) |
| App at onset | 26.810.41047 (build 6570) |
26.810.41047 (build 6570) |
| Codex Framework | 151.0.7922.137 |
151.0.7922.137 |
| Embedded codex-cli | 0.148.0-alpha.9 |
0.148.0-alpha.9 |
| Computer Use runtime | 26.812.1000717 |
not captured before rollback |
Machine B hit the same failure independently the same day (2026-08-14), and the same rollback
resolved it. The two onset installs are not merely the same version string — sky.node on
Machine B is 1210752 bytes hashing 1e468ee72cc1e402…, byte-identical to Machine A's 41047.
Machine B's Computer Use runtime version was lost when the rollback re-provisioned it, so that
one cell is genuinely unknown rather than assumed to match.
Scope of Machine B's evidence. We did not re-measure the spawn storm on Machine B. The app had already crashed and was not running when we started, and reproducing would have cost another ~600 spawned processes for information Machine A had already established. Machine B was identified by version match plus crash-signature match, then rolled back and monitored. Its value here is as an independent confirmation of the crash signature, the rollback, and the Sparkle re-enable below — not as a second measurement of spawn rate.
macOS version is not a variable. Machine A crossed from 26.6 to 27.0 beta mid-investigation:
the first three crash reports are stamped macOS 26.6, the remainder macOS 27.0, with no
change in spawn rate, accumulation pattern, or crash threshold. We have not seen 27.0 reported
elsewhere in the thread.
- Spawn rate ~2/sec from launch while idle, accelerating to ~6/sec as the count grows.
- Helpers stay alive and accumulate — process start times are evenly spread across the window. This is live accumulation, not churn and not zombie reaping failure.
- App thread count grows ~1:1 with spawns (74 → 169 threads as helpers went 6 → 111).
- Crash at ~600–682 concurrent helpers, ~4–4.5 minutes after launch. 7 of 7 crashes in one afternoon fell in that band; the tightness of that clustering is what first suggested a hard resource limit rather than a heuristic.
Representative sequence from one launch (15s sampling):
13:28:00 helpers=8 13:29:33 helpers=198
13:28:15 helpers=40 13:30:18 helpers=342
13:28:30 helpers=71 13:30:48 helpers=521
13:28:46 helpers=103 13:31:04 helpers=607
13:29:01 helpers=133 13:31:19 app crashed (682)
EXC_BREAKPOINT (SIGTRAP), in two of seven cases SIGABRT, always on a thread named
computer-use. Symbolicated bottom of the crashing stack:
node::worker::Worker::Run()
→ node::NewIsolate()
→ v8::Isolate::Initialize()
→ EXC_BREAKPOINT
The fatal abort lands on isolate creation. This is a slightly different presentation from
the node::OOMErrorHandler path reported by others in #38455, but the same
resource-exhaustion family — consistent with the thread-count growth above, and with the
launchservicesd 512-dispatch-thread exhaustion and kernel panics reported independently in
#38760 and #38697.
The abort site varies; the thread does not. Both of Machine B's crashes (12:36:52 and
17:03:29 local, 2026-08-14) were EXC_CRASH (SIGABRT), and neither landed on NewIsolate.
They aborted further up, inside the worker's message loop:
node::worker::Worker::Run()
→ node::SpinEventLoopInternal()
→ uv_run() → uv__io_poll() → uv__async_fork()
→ node::worker::MessagePort::OnMessage()
→ node::AsyncWrap::MakeCallback()
→ node::InternalMakeCallback()
→ v8::Function::Call()
→ SIGABRT
So across nine crashes on two machines we have three presentations — SIGTRAP on
Isolate::Initialize, SIGABRT on Isolate::Initialize, and SIGABRT inside
MessagePort::OnMessage — with node::worker::Worker::Run() at the bottom and a thread named
computer-use in all nine. If you are triaging by exception type or by top frame you will
split one bug into three. The thread name is the stable discriminator.
Each item was applied, then re-tested with a clean relaunch and process monitoring. None changed the spawn rate measurably. We are listing these mainly so the team can rule out user configuration as a factor.
| Attempt | Result |
|---|---|
tccutil reset Accessibility for app + helper bundle IDs; granted the Accessibility prompt when it then appeared; re-added "Codex Computer Use" to Screen Recording |
No change. Our initial hypothesis was a stale TCC record after reinstall — one machine had shown the Computer History permission dialog and the other never did. Permissions turned out to be unrelated to the crash. |
| Computer Use toggled off in Settings | No change |
| Computer History toggled off in Settings | No change |
[features] computer_use = false in ~/.codex/config.toml |
No change to spawning — though the app clearly read it: on next launch it removed its own [mcp_servers.computer-use] and [plugins."computer-use@openai-bundled"] entries. Worth noting as a UX inconsistency: the setting is honored in config bookkeeping but not in the spawner. |
[desktop] computerUseAlwaysHidePictureInPicture = true |
No change. We tried this because it resolved the milder 26.730.61639 PiP respawn variant reported on the community forum on Aug 5. |
Deleted ~/.codex/computer-use/ |
No change — the runtime was re-provisioned within seconds of launch and spawning resumed. |
| Clean room: killed every codex-family process system-wide (app, app-server, helpers, orphans), unloaded unrelated launchd agents, verified zero, relaunched | No change — 4 → 101 helpers in 60 seconds. This is the result we'd most highlight: the loop requires no state or process outside the app binary. |
| Killed accumulated helpers on a 20s interval while the app ran | This did prevent the crash. Over a 10.5-minute run the helper count stayed capped at 25–37, and the app's thread count plateaued around 105 then declined to ~90. The same machine died in ~4 minutes without it. |
That last row may be the most useful diagnostic signal here: the worker threads are released when their child processes die. They are not orphaned. A fix that merely bounds the spawn loop — or that reaps on handshake failure — should reclaim them without further plumbing.
We ran that reaping loop as a stopgap for a while. It costs roughly 20–50% of one core in permanent churn, so it isn't something we'd want to keep running, and given the kernel-panic reports elsewhere we stopped relying on it.
Three builds shipped in ~36 hours (41047 Aug 13, 50856 Aug 14, 52044 Aug 15), then the
cadence stopped. Comparing the first and last locally:
| 26.810.41047 | 26.810.52044 | |
|---|---|---|
Contents/Resources/native/sky.node |
1210752 bytes, 1e468ee72cc1e402… |
1210752 bytes, b6248745c81c7013… |
embedded codex-cli |
0.148.0-alpha.9 |
0.148.0-alpha.9 |
| Computer Use runtime | 26.812.1000717 |
26.812.1000717 |
The native module was rebuilt — same size, different hash — while the embedded CLI and the
Computer Use runtime are unchanged. That matches every user report that 50856 and 52044
remain affected. As an outside observer, the Computer Use runtime version is the only field we
found that would distinguish a real fix from a rebuild.
We rolled back and set:
defaults write com.openai.codex SUEnableAutomaticChecks -bool false
defaults write com.openai.codex SUAutomaticallyUpdate -bool false
Re-checking two days later: both keys had been reset to 1, SULastCheckTime showed a
fresh check, and 26.810.52044 was fully downloaded (1.9 GB) and staged in
~/Library/Caches/com.openai.codex/org.sparkle-project.Sparkle/Installation/, ready to install
on next quit. Only 39 hours of uninterrupted app uptime had kept it from landing.
We mention it because it interacts badly with the severity of this particular bug: users who believe they have pinned themselves to a safe build may be silently re-updated into one that, per #38760 and #38697, can panic the machine. The app rewriting its own preferences domain is presumably intentional; the interaction with a known-bad release is the concern.
This reproduced independently on Machine B, and faster. Both keys were written false at
17:15 local on 2026-08-14, immediately after that machine's rollback. Re-checking at 13:40 local
on 2026-08-16:
| Machine A | Machine B | |
|---|---|---|
SUEnableAutomaticChecks |
reset to 1 |
reset to 1 |
SUAutomaticallyUpdate |
reset to 1 |
reset to 1 |
| Time to reset | ~2 days | ≤23 h (SULastCheckTime = 2026-08-15 16:35:53 UTC) |
| Staged build | 26.810.52044, 1.9 GB |
26.810.52044, 1.9 GB |
| What held it off | 39 h app uptime | 26 h app uptime |
Two details from Machine B worth having:
Only the app's own domain is rewritten. We set the keys in both com.openai.codex and
com.openai.chat as a belt-and-braces measure. com.openai.codex — the actual
CFBundleIdentifier of the shipping app — came back as 1/1. com.openai.chat was still
0/0, untouched. So this is the running app rewriting its own preferences, not an external
agent or a stale domain, which narrows where a fix would go.
The staged payload is identical. Same version, same 1.9 GB, same
Installation/<id>/<id>.810.52044.zip layout on both machines. Machine B was pinned and offline
from the updater by intent, and Sparkle still fetched and unpacked a full copy of a build that
the same document identifies as affected.
The practical shape of this for users: on both machines the only thing that kept a staged,
known-affected build from landing was that the app never stopped running. We did not test
whether an abnormal termination triggers the staged install the way a clean quit does — that is
the open question we'd most like answered, because on 26.810 abnormal termination is the
app's steady state.
Rolled back to 26.727.51351 on both machines — the last build we could find that predates
this bug class, and specifically earlier than 26.730.61639, where the PiP respawn behavior
first appears publicly. (Some rollback advice in the thread points at 26.803.x; that line has
its own crash-on-launch report in #38037, which is why we went further back.)
Result: 39+ hours uptime, one helper process, thread count flat at ~66, zero crashes.
Computer Use functions normally there, and the app re-provisioned a version-matched runtime
(26.727.1000550) on its own.
Machine B landed in the same place from an independent rollback: 26+ hours uptime,
SkyComputerUseService flat at 1, thread count 62–73 across a 2-minute sampling window with no
upward trend, zero crash reports since. It re-provisioned the identical runtime
(26.727.1000550) unprompted, which is a small point in favor of that runtime version being
the meaningful field to watch.
For the sampling window immediately after the swap (15s interval):
17:11:51 sky=0 threads=64 17:13:06 sky=1 threads=62
17:12:06 sky=1 threads=65 17:13:21 sky=1 threads=62
17:12:21 sky=1 threads=65 17:13:36 sky=1 threads=73
17:12:36 sky=1 threads=62 17:13:52 sky=1 threads=66
17:12:51 sky=1 threads=64
Compare against the 26.810 sequence in What we measured: same sampling interval, same
machine class, 607 helpers by the four-minute mark there versus 1 here.
This is a holding position, not a resolution — it costs us everything shipped since late July, including Computer History.
For anyone else who needs to get to this state, 26.727.51351 is still served from OpenAI's own
CDN and is notarized, so it installs without any Gatekeeper override:
https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.727.51351.zip
https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.727.51351.zip
SHA-256, cross-checked against Homebrew's audited cask history (commit a1ac4f8212) and
re-verified on download:
arm64 8f3fc87e634332fddc711e5221eb2af554f5f6ecb04e6a69b3d10e01f4f196c8
x64 94120e6ff4f0da1224283cf90bdc51f3c352c0538c617f68747c39fcf6ef8ce7
spctl -a -vv on the extracted bundle returns accepted, source=Notarized Developer ID,
Developer ID Application: OpenAI OpCo, LLC (2DC432GLL2). Keep the outgoing bundle rather than
deleting it — having 26.810.41047 on disk is what let us do the byte-level sky.node
comparison above after the fact.
Worth flagging since it's adjacent and long-lived. On 26.727.x, one SkyComputerUseClient —
plus one instance of every configured stdio MCP server — is stranded per idle conversation,
at roughly 0.25–0.5/hour, each consuming ~1–1.3% of a core indefinitely.
Reading the open-source tree: MCP servers are spawned per conversation thread, and teardown runs
via McpConnectionSet::shutdown on session shutdown. thread_lifecycle.rs does implement idle
unloading (THREAD_UNLOADING_DELAY, 30 min; PRs #17223 and #17398) — but it is gated on
has_subscribers == false. Our observation is that the desktop client holds one long-lived
app-server connection and does not appear to unsubscribe idle conversations, so that path never
runs. The supporting evidence is negative but clean: the unload path logs
"has no subscribers and is idle; shutting down", and that string appears zero times across
our entire ~/Library/Logs/com.openai.codex/2026/** history, while the accumulation is
demonstrably live. Umbrella issue: #25744.
Killing the stranded children doesn't help — the still-live session respawns replacements 1:1, which is consistent with them being supervised rather than leaked.
Machine B let us separate those two variables, because it ran 26 hours on 26.727.51351 with
almost no interactive use. At the 26-hour mark:
SkyComputerUseService 1
SkyComputerUseClient 3 elapsed 26:07:30, 26:06:07, 26:04:19 (0.8–1.0% CPU each)
node_repl 3 the one remaining enabled stdio MCP server
Every stranded client's elapsed time puts its start within ~3 minutes of app launch, and the
count did not move over the following 26 hours. So the per-hour framing above ("0.25–0.5/hour")
is better read as a consequence of how often conversations get opened than as a clock-driven
leak. An idle app does not accumulate; an app that opens conversations does. That is consistent
with the thread_lifecycle.rs reading — the unit of leakage is a conversation thread that never
loses its last subscriber — and it means the practical severity depends entirely on usage
pattern, which may explain the spread in reports.
Machine B's codex config had three enabled stdio MCP servers at onset (context7,
firecrawl, node_repl; chrome-devtools and computer-use were present but
enabled = false, and deepwiki is a remote url entry). Removing the two removable ones
dropped the stranded MCP process count to exactly conversations × remaining stdio servers —
3 above — with the remote entry contributing nothing. The relationship is linear and the
remote-vs-stdio distinction holds.
One wrinkle for anyone estimating blast radius: an MCP server configured as command = "npx"
costs two processes per conversation, not one — the npm exec wrapper plus the actual
server it execs:
npm exec @upstash/context7-mcp ← wrapper, stays resident
node .../node_modules/.bin/context7-mcp
So a user with four npx-configured stdio servers strands eight processes per conversation plus
the client, not four. Converting entries with an HTTP endpoint to remote url = form removes
them from the multiplier entirely; that is the only user-side mitigation we found that doesn't
involve restarting the app on a schedule. It reduces the per-conversation cost — it does not
stop conversations from being stranded, and the SkyComputerUseClient count still grows.
- Is the
~600ceiling thelaunchservicesd512-dispatch-thread limit surfacing as a V8 isolate-allocation failure, or two independent limits reached at similar times? The kernel-panic reports suggest the former. - Does the retry path have any failure counter at all? From the outside the spawn rate never decays, which is what #38841 reports finding in the JS layer.
- Is there a supported way to pin a desktop version, for users who need to stay off a bad build? (#38022 requests this.) Today it requires fighting the updater.
- Does an abnormal termination install a staged Sparkle update, or only a clean quit? Both machines had a known-affected build staged and unlaunched purely because the app stayed up. If crashes do trigger installation, the pin is not merely fragile for this bug class — it is specifically defeated by it.
- Why does the abort site move between
Isolate::InitializeandMessagePort::OnMessagewhile the thread name stays constant? If both are the same allocation failure surfacing at different points, the nine crashes are one bug; if not, thecomputer-usethread has a second failure mode worth separating.
- openai/codex#38455 — canonical thread for the 26.810 spawn storm
- openai/codex#38760, openai/codex#38697 — kernel panics; runtime segfault at launch
- openai/codex#38841 — missing circuit breaker in the JS reconnect callback
- openai/codex#38515, openai/codex#38771, openai/codex#38744 — dispatch-limit and 52044 reports
- openai/codex#38037 — 26.803.x crash-on-launch
- openai/codex#25744 — umbrella: MCP / Computer Use helper accumulation
- openai/codex#38022 — feature request: pin desktop version