Skip to content

[macOS] CODEX_HOME symlink triggers unbounded computer-use worker leak and V8 OOM in ~90s #39732

Open

Description

@TahitianBlue

ChatGPT desktop (macOS) aborts ~90 s after every launch: unbounded computer-use worker-thread leak when CODEX_HOME is reached through a symlink

Related reports with the same worker-leak/OOM symptom but without this symlink-vs-real-path isolation: #38813, #39110, #38769.

Summary

If CODEX_HOME is given as a path that traverses a symlink, the app leaks Node worker_threads named computer-use at roughly 4/s from startup. None are ever released. At ~320 total threads V8 can no longer obtain memory and calls node::OOMErrorHandler(), aborting the whole process. This happens with no user interaction — the app dies before it is usable.

Passing the resolved real path for the exact same directory makes the app completely stable. The data is identical; only the string differs.

User impact

The app-server log database shows the ~85 s crash-restart pattern confined to a single
day, 2026-08-20: 32 process sessions, 24 of them lasting between 60 s and 130 s, median
85 s. The preceding days show normal long-lived sessions (2026-08-14: 7 sessions, median
3.9 h, none under 130 s). This matches the timestamp at which Codex Computer Use.app
was updated to 26.818.1000790 — 20:51 local time, six minutes before the first crash at
20:52:37.

After applying the real-path CODEX_HOME workaround, 2026-08-21 shows 4 sessions with a
median span of 12.8 h.

Environment

App ChatGPT 26.818.21641 (build 6849), bundleID com.openai.codex
app-server 0.148.0-alpha.21
OS macOS 26.5.2 (25F84)
Hardware Mac mini (Apple M4), 16 GB RAM
Layout /Users/<user>/.codex is a symlink → /Volumes/<ExternalSSD>/.codex (APFS, external SSD, permanently mounted)

Symptom

14 consecutive crashes in 76 minutes with no user interaction, then reproduced on demand. Time from process launch to abort, from the .ips reports:

89.2s  83.6s  81.5s  82.9s  82.1s  90.5s  90.9s  84.4s  89.9s  88.2s

Crash signature

exception    : EXC_CRASH (SIGABRT)   [some reports: EXC_BREAKPOINT / SIGTRAP]
legacyInfo   : {"threadTriggered": {"name": "computer-use"}}
asi          : {"libsystem_c.dylib": ["abort() called"]}

Faulting stack:

node::OOMErrorHandler(char const*, v8::OOMDetails const&)
v8::CppHeap::CollectGarbageInYoungGenerationForTesting(...)
v8::ScriptCompiler::StartStreaming(...)
node::WorkerThreadsTaskRunner::WorkerThreadsTaskRunner(int, node::PlatformDebugLogLevel)
_pthread_start

Thread census, consistent across all 15 crash reports collected:

total= 323  computer-use= 187  V8Worker= 3  libuv-worker= 4
total= 320  computer-use= 185  V8Worker= 3  libuv-worker= 4
total= 322  computer-use= 185  V8Worker= 3  libuv-worker= 4
total= 319  computer-use= 184  V8Worker= 3  libuv-worker= 4
  ... (184–187 every time)

This is not a large-heap OOM

Running the binary directly and capturing stderr shows the isolate heap is tiny at the moment of death:

<--- Last few GCs --->
[pid:0x...]   43 ms: Scavenge 2.3 (4.2) -> 2.0 (4.8) MB, pooled: 0.0 MB
[pid:0x...]  197 ms: Scavenge 7.0 (8.9) -> 6.1 (9.4) MB, pooled: 0.0 MB
[pid:0x...]  254 ms: Scavenge 7.6 (10.4) -> 7.0 (10.7) MB, pooled: 0.0 MB

FATAL ERROR: MarkCompactCollector: young object promotion failed
             Allocation failed - JavaScript heap out of memory

7 MB of heap, on a machine with ample free RAM. The allocation fails because ~186 live worker isolates have exhausted what the process can reserve. Dispatch Thread Soft Limit Reached also appears in the reports.

Thread growth is linear from launch until the ceiling is hit:

t+15s  threads=133   t+45s  threads=219   t+75s  threads=299
t+30s  threads=180   t+60s  threads=258   t+90s  ABORT

Reproduction — the deciding variable

Same directory, same inode, same data. Only the CODEX_HOME string differs. [IpcRouter] I am the router is logged once per IPC socket connection, so it is a direct proxy for the leak rate.

Run CODEX_HOME Result IPC connections in 90 s
F /Users/<user>/.codex (symlink) crash at 90 s 434
G /Volumes/<SSD>/.codex (real path) survived 105 s 4
G2 real path survived 105 s 4
F2 symlink crash at 105 s 434
G3 real path survived 105 s 4

Order-independent (G2 → F2 → G3 alternating gives the same verdict). With the real path the process is flat at 59–61 threads and ~520 MB RSS for 3.5 minutes and beyond, with the computer-use plugin fully enabled.

Note that a normal Dock/Finder launch has CODEX_HOME unset, so the app derives ~/.codex — the symlink form — and always crashes.

Minimal repro

mv ~/.codex /some/other/volume/.codex
ln -s /some/other/volume/.codex ~/.codex
open -a ChatGPT          # aborts after ~90 s, every time

launchctl setenv CODEX_HOME /some/other/volume/.codex
open -a ChatGPT          # stable

Supporting evidence for a path-comparison mismatch

state_5.sqlite stores threads.rollout_path in both forms:

73 rows  /Users/<user>/.codex/sessions/...      (symlink form)
 4 rows  /Volumes/<SSD>/.codex/sessions/...     (real form)

and the app-server logs, on every thread/list:

WARN codex_rollout::state_db  state db discrepancy during list_threads_db: stale_db_path_retained
WARN codex_rollout::state_db  state db list_threads returned stale rollout path for thread <id>: /Users/<user>/.codex/sessions/...

Two spellings of the same file never compare equal, so the retained-path check never converges.

Where the threads come from

From Contents/Resources/app.asar, the exported requestComputerUseWorker handler:

b = async e => {
  let t = new P9(`computer-use`, { startupConfig: i });
  w(t);
  let n = a.subscribe(e => { t.emitAppEvent(e) }),
      r = !1,
      o = () => { r || (r = !0, n.dispose(), m.delete(t), t.dispose()) };
  m.set(t, o);
  try {
    ...
    t.startWorker();
    let r = await t.requestFromHost(e);   // <-- never settles
    ...
  } finally { o() }                        // <-- therefore never runs
}

and the worker's thread name is the manager id:

let f = new M.Worker(t, { name: this.id, workerData: d, ... });

which is why the leaked threads are all named computer-use. A second path, T(requestId), keeps managers in a map keyed by requestId and has the same exposure.

The disposal path is reachable only after the awaited request resolves or rejects. There is no timeout, so a request that never settles leaks one Node worker — with its own V8 isolate — permanently. Whatever retries the request does so about four times per second, which is enough to kill the process in 90 seconds.

Ruled out

Each of these was tested by reconstructing a CODEX_HOME on the internal disk and adding components back additively, 105 s per configuration. None of them reproduced the crash, including the configuration holding essentially the entire home directory:

  • the computer-use, browser-use and chrome plugins — also renamed away on disk in the real home, and it still crashed
  • session-history size — 809 MB across 70 rollout files, including a single 499 MB rollout and threads with tokens_used over 178,000,000
  • .codex-global-state.json, state_5.sqlite, session_index.jsonl, sessions/
  • plugins/ (306 MB), .tmp/ (161 MB), skills/
  • logs_2.sqlite, memories_1.sqlite, goals_1.sqlite, queue_1.sqlite, thread_history_1.sqlite, models_cache.json, caches
  • MCP server configuration (node_repl, codex_apps)

Disabling the plugin through config.toml ([plugins."computer-use@openai-bundled"] enabled = false) and through computer-use-bundled-plugin-auto-install-disabled in .codex-global-state.json had no effect — the app still crashed at 88.9 s and 90 s respectively. The app did honour the setting (it removed SKY_CUA_SERVICE_PATH from the node_repl env block), but the worker spawn loop lives in the Electron main process and is not gated by it.

Workaround

launchctl setenv CODEX_HOME /Volumes/<SSD>/.codex

Persisted with a LaunchAgent that runs the same launchctl setenv at login.

Suggested fixes

  1. Canonicalize CODEX_HOME at startup (realpath) and store only resolved paths, or compare paths after resolution rather than as strings. This addresses the trigger and the stale_db_path_retained warnings together.
  2. Bound the computer-use worker lifecycle independently of the awaited request — a timeout plus disposal on the failure path, and a cap on concurrent computer-use managers. As written, any request that never settles leaks a Node worker with its own V8 isolate, so a retry loop can take the whole app down regardless of what caused the request to hang. The second fix matters even if the first lands, because it turns a hung request into a degraded feature rather than a crash.

Side issue found while investigating

A thread created from the Chrome extension side panel could not be deleted from the UI: the row stayed in state_5.sqlite with archived = 0 after the deletion attempt reported an error. The thread had tokens_used = 0 and has_user_event = 0, i.e. it was never run. It was written by CLI version 0.147.0-alpha.6.5. Removing it required deleting the row, the rollout file, the session_index.jsonl entry, and a heartbeat-thread-permissions-by-id key in .codex-global-state.json by hand. This may or may not share a root cause with the above.

Activity

  1. TahitianBlue commented on Aug 20, 2026

    @TahitianBlue
    Author

    Possible unintended quota/credit consumption

    There may also have been account-level usage impact. At approximately August 14, 2026, 8:20 PM JST, the weekly usage allowance became exhausted unexpectedly, despite the separate five-hour limit and without usage that would reasonably explain consuming an entire week's allowance at once. The desktop app then remained unusable until the real-path workaround was identified on August 20.

    The local evidence proves a retry/worker-spawn storm (about 434 IPC connections per 90 seconds in the failing configuration), but it cannot establish whether those retries reached OpenAI services or were counted against the account. Please correlate the account's server-side usage telemetry for the incident window with the crash/retry timestamps and determine whether this bug caused unintended quota or credit consumption.

    If unintended consumption is confirmed, I request a refund or restoration of the equivalent credits/usage allowance. A substantial amount of additional time and third-party Claude Code usage was also required to isolate and document this deterministic product bug.

    I understand that billing adjustments may need to be handled privately by Support; please advise or route this report to the appropriate team.

  2. galligan commented on Aug 20, 2026

    @galligan

    Independent replication — confirmed, with a known-bad control

    Your symlink finding holds up. I reproduced it on a Mac that had been crashing 7 out of 7
    launches, using 26.810.41047 as a control build and changing exactly one variable:

    ~/.codex Result (idle, same machine, same script)
    symlink → ~/.config/codex 3 → 73 helpers in 27 seconds, app threads 71 → 138
    real directory (.config/codex inverted to point at it) 1 helper, flat for 903 seconds, threads declining 67 → 60

    33× the time-to-storm on the same binary. Then migrated a second machine the same way and moved
    both to 26.818.22352 with Computer History enabled (runtime 26.819.1000816) — 1 helper,
    flat, zero crashes since. Your decompilation of the missing timeout/cap in
    requestComputerUseWorker matches everything I measured.

    Two additional data points that may help others find themselves in this:

    1. It explains the "two machines, one account" reports. Several of us (myself included)
    suspected account-level or server-supplied state, because the storm followed one login across
    multiple Macs. But my machines also share dotfiles — both had the identical
    ~/.codex -> ~/.config/codex layout, created at different times. Two machines, one dotfile
    layout
    . That's a much duller explanation and it fits the evidence better.

    2. The condition is visible statically, before running anything. On my machine:

    • the same hooks.json was registered twice in config.toml, once under each spelling —
      identical trusted_hash values, because it's one file with two names
    • state_5.sqlite held 4,865 thread rows under /Users/mg/.codex/... versus 4 under
      /Users/mg/.config/codex/...
    • SKY_CUA_SERVICE_PATH used one spelling while the notify hook used the other

    Anyone can check for the duplicate-hook tell with a quick grep of config.toml, or compare
    readlink ~/.codex against CODEX_HOME.

    One migration caveat worth flagging for anyone following your fix: the majority of stored
    paths are usually in the symlink spelling (4,865 vs 4 for me), so inverting the symlink —
    making ~/.codex the real directory — leaves nearly all existing state canonically correct,
    rather than orphaning it. Whereas setting CODEX_HOME to the resolved .config path
    canonicalizes toward the spelling that only a handful of records use. Both stop the storm; the
    inversion needs far less rewriting afterward.

    Thanks for the decompilation and the clean A/B — that was the thing that broke this open after
    several of us spent days on permissions, config flags, and account theories.

    Fuller write-up if useful: https://gist.github.com/galligan/c8a64ec15b89e11ac0421b1b117056c0

  3. saigyouyou commented on Aug 24, 2026

    @saigyouyou

    Independent confirmation on 26.818.31338, with direct native matcher evidence

    I independently confirmed this issue on the same Mac using two application builds:

    • Stable control: ChatGPT 26.803.81509 (build 6415), Computer Use 26.804.1000633
    • Affected: ChatGPT 26.818.31338 (build 6892), Computer Use 26.819.1000816
    • Original layout: ~/.codex -> /Users/<user>/projects/_agents/codex

    The affected build produced a hang report containing:

    • 302 SkyComputerUseService processes
    • 120 main-process threads named computer-use
    • main-process footprint around 3.9 GB
    • approximately 6.6 GB across the helper processes
    • Processes reached dispatch thread soft limit (64)

    I also directly called computerUseServiceProcessMatchesExecutablePath
    from the bundled sky.node native addons using a live service PID.

    Both the old and new native addons returned:

    • expected path through ~/.codex: false
    • resolved real executable path: true

    lsof reported the running executable using the resolved path. This directly
    confirms that the service identity check compares the lexical path without
    canonicalizing the symlink.

    I then inverted the layout:

    • ~/.codex is now the real directory
    • /Users/<user>/projects/_agents/codex -> ~/.codex

    The crash loop stopped without disabling Computer Use.

    The matcher behavior also exists in the older stable addon, so the path mismatch
    predates the regression. The newer desktop lifecycle/retry path appears to turn
    that mismatch into the unbounded spawn loop.

    This supports both proposed fixes:

    1. Canonicalize paths before service identity comparison.
    2. Add single-flight spawning, bounded retries/backoff, helper cleanup, and a
      circuit breaker regardless of the initial handshake failure.
  4. LeandroLopina commented on Aug 29, 2026

    @LeandroLopina

    Related independent report: same unbounded/OOM class, different trigger

    This report is compatible with our incident at the failure-class level, but not with the exact symlink trigger, so I want to keep the distinction clear:

    • On our affected Mac, ~/.codex is currently a real directory, realpath ~/.codex is /Users/<user>/.codex, and CODEX_HOME is unset.
    • Therefore, I am not claiming that the lexical symlink mismatch described in this issue caused our crashes.
    • What does match is the broader absence of lifecycle bounds, cleanup, backoff, and circuit breakers around Desktop background work, followed by node::OOMErrorHandler() and an application abort.

    Our observed incidents

    On ChatGPT/Codex Desktop 26.818.21641 with bundled app-server 0.148.0-alpha.21, the main codex process reached 48.89 GB on a Mac with 18 GB physical RAM, while macOS used 39.50 GB of swap. After the internal process restarted, it again reached 34.31 GB with 25.64 GB of swap.

    The task involved had a 36.8 GB local rollout JSONL. Fresh launch and resume repeatedly spent about 75–77 seconds in thread resume and turn-list operations, and memory dropped abruptly when the operation ended or was interrupted. All available Codex credits were also exhausted during this incident. Local evidence cannot prove whether retry activity was billed, but the quota impact closely resembles the possible unintended-consumption report already documented above.

    On August 26, three separate tasks then crashed on Desktop 26.820.60940 build 7119, approximately 56.4 s, 51.5 s, and 45.6 s after conversation creation. All three native crash reports had the same immediate signature: node::OOMErrorHandler in the internal git thread, followed by SIGABRT. The first two also showed rollout-file timing failures, and repeated Git/GitHub metadata operations were active around the crashes.

    Still present after closing and updating

    The app and its processes were closed and it subsequently updated to Desktop 26.825.41651 build 7345, bundled CLI 0.151.0-alpha.7.1. The resource-management behavior does not appear resolved.

    At the time of this comment, a Codex child process was still running:

    /opt/homebrew/bin/git ... add --sparse --pathspec-from-file=- --pathspec-file-nul
    RSS: ~947 MB
    CPU: ~99%
    Elapsed: 2h43m
    Parent: ChatGPT/Codex Desktop main process
    

    This is not a computer-use worker and may be a separate code path, but it is consistent with our earlier git-thread OOM crashes and with the general need identified in this issue for bounded concurrency, timeouts, cleanup, backoff, and circuit breakers independent of the original trigger.

    We have already provided OpenAI Support with three .ips reports, exact timestamps, sanitized app-log windows, and sanitized session lifecycle records. The repeated requests for additional user-side reproduction and account checks are imposing a substantial operational burden while active work remains at risk. Multiple independent reports in this issue show that this is not isolated to one user or one machine, and recent Desktop updates have not made our case stable.

    Please let us know whether maintainers want this tracked as a separate issue for the non-symlink git/history-loading path. I can provide sanitized structural evidence without publishing confidential project content.

  5. karan-w commented on Sep 1, 2026

    @karan-w

    I reproduced this on the same desktop build and crash signature.

    Environment (redacted)

    • ChatGPT macOS 26.818.21641 (build 6849), bundle com.openai.codex
    • macOS 26.3 (25D125), Apple Silicon (MacBook Pro, M4 Pro, 24 GB)
    • Normal Finder/Dock-style launch; no user interaction required
    • ~/.codex is a symlink (target intentionally omitted)

    Observed run

    • Launched 2026-09-01 18:38:11 +0400; self-aborted about 110 seconds later.
    • Crash: EXC_CRASH / SIGABRT, Abort trap: 6; triggered thread V8Worker.
    • 320 total threads at termination: 185 named computer-use, 3 V8Worker, 4 libuv-worker.
    • 154 computer-use threads had the repeated blocking path: __ulock_wait -> _dlock_wait -> _dispatch_group_wait_slow -> AESendMessage -> ffi_call_SYSV.

    Faulting stack

    __pthread_kill
    pthread_kill
    abort
    node::OOMErrorHandler(char const*, v8::OOMDetails const&)
    reading_mode$cxxbridge1$194$parse_distilled_html
    reading_mode$cxxbridge1$194$parse_distilled_html
    v8::CppHeap::CollectGarbageInYoungGenerationForTesting(...)
    v8::Context::SetAbortScriptExecution(...)
    v8::ScriptCompiler::StartStreaming(...)
    uv_os_getpid
    node::WorkerThreadsTaskRunner::WorkerThreadsTaskRunner(...)
    _pthread_start
    thread_start
    

    The installed app verifies as signed, notarized, and valid. I also observed only 3.5 GiB of local free disk space; that may exacerbate pressure but the crash report identifies the V8 worker/thread-leak path. The redacted .ips report is available privately if useful.

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

    appIssues related to the Codex desktop appbugSomething isn't workingcomputer-useperformance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions