Codex Desktop leaves orphaned zsh shell-snapshot processes burning ~100% CPU

Open 💬 3 comments Opened May 31, 2026 by acambitsis

Summary

Codex Desktop intermittently leaves orphaned shell snapshot subprocesses running under launchd. Each process is /bin/zsh -lc ... running Codex's shell snapshot capture command, and each burns roughly one full CPU core indefinitely until manually killed.

This appears distinct from existing CLI orphan/native-process issues because the runaway process is not the codex native binary or Node wrapper. It is the shell snapshot subprocess itself.

Environment

  • Codex Desktop: 26.527.31326, build 3390
  • Codex CLI/runtime version recorded locally: 0.133.0
  • macOS: 26.5 (25F71)
  • Architecture: arm64
  • Shell: /bin/zsh
  • App bundle: /Applications/Codex.app

Observed behavior

Multiple orphaned /bin/zsh processes appear with PPID 1, each consuming about 97-100% CPU.

Observed process command shape:

/bin/zsh -lc if [[ -n "$ZDOTDIR" ]]; then
  rc="$ZDOTDIR/.zshrc"
else
  rc="$HOME/.zshrc"
fi
[[ -r "$rc" ]] && . "$rc"
print '# Snapshot file'
print '# Unset all aliases to avoid conflicts with functions'
print 'unalias -a 2>/dev/null || true'
print '# Functions'
functions
...
setopt
alias -L
export -p

Concrete examples from this machine:

  • 2026-05-30: four orphaned snapshot processes, each around 98-100% CPU for about 45-46 minutes, total roughly 400% CPU. Killing only those PIDs immediately improved CPU idle from about 36% to about 69%.
  • 2026-05-31: two more orphaned snapshot processes:
  • PID 77383, about 97% CPU, running about 22 minutes
  • PID 4502, about 97% CPU, running about 8 minutes
  • After killing those exact PIDs, pgrep -lf "print '# Snapshot file'" returned nothing and load average began dropping.

Diagnostics performed

The same shell snapshot pattern completes quickly when run directly with a hard timeout:

time perl -e 'alarm shift; exec @ARGV' 10 /bin/zsh -lc '[[ -r "$HOME/.zshrc" ]] && . "$HOME/.zshrc"; functions >/dev/null; setopt >/dev/null; alias -L >/dev/null; export -p >/dev/null; print ok'

Result: completed successfully in about 1s.

Snapshot files are being written under:

~/.codex/shell_snapshots/

Recent files included:

019e7a0e-5d2e-77a0-b27b-ecb6a3c1739f.1780213284925867000.sh
019e7cfc-2f3f-7ce3-aef9-e0aab975fba9.1780213362784461000.sh

This suggests the capture often reaches the output stage, but the subprocess may not exit or Codex may lose cleanup/ownership.

I also saw stale process-manager entries in:

~/.codex/process_manager/chat_processes.json

And archived Codex session output showed similar previously orphaned snapshot commands, including long-running examples.

Expected behavior

Codex Desktop should ensure shell snapshot subprocesses terminate promptly, and should kill/timeout them if snapshot capture hangs or if the parent session/thread is interrupted or restarted.

Actual behavior

Shell snapshot subprocesses can become orphaned under launchd and continue spinning indefinitely at about one CPU core each.

Impact

  • Machine runs hot.
  • Multiple orphaned snapshot processes accumulate across sessions.
  • CPU usage can reach several full cores until manually cleaned up.

Related issues

These may be adjacent but appear to cover different process types/failure modes:

  • openai/codex#13928: Codex native binary persists after session exit and consumes CPU
  • openai/codex#14962: CLI orphan process pairs after terminal close/SIGHUP
  • [#10883](<https://github.com/openai/codex/issues/10883>): shell snapshot compatibility with runtime shell

Suggested next diagnostic

If this recurs, I can collect a sample before killing the process:

sample <pid> 5

That should identify where zsh is spinning.

View original on GitHub ↗

3 Comments

nguyentrungtu · 2 months ago

Confirmed this is a thing on my machine. The fan was on for no reason, I checked and there were 5-7 zsh processes that had to be killed manually.

scab24 · 24 days ago

Same issue from the CLI

I hit the same problem through Codex CLI on macOS, not the desktop app. Here is what I found:

  • MacBook Pro M1 Pro, macOS, zsh
  • Incident: Codex CLI 0.145.0
  • Now running: Codex CLI 0.146.0
  • Eleven snapshot zsh processes passed my full identity checks. One earlier pilot was verified separately from its PPID, TTY, children, command shape, and snapshot markers.

One process had been using most of a core for more than an hour:

PID       PPID TTY  %CPU ELAPSED  COMMAND
69976        1 ??   78.1 01:25:08 /bin/zsh

What helped

  • I revalidated each matching PID immediately before sending an individual SIGTERM to that exact process.
  • Result: matched=11 skipped=0 terminated=11 survived=0. The pilot had already been terminated separately.
  • The next scan found zero matches. CPU idle recovered to 84.12%, then 92.7%.
  • For noninteractive shell startup, I changed NVM loading to:
source "$NVM_DIR/nvm.sh" --no-use

Interactive NVM still works, and I have not seen new matches since making this change. That does not prove NVM caused the issue, and it does not fix Codex cleanup.

What may be happening

This fits the failure, although a descendant must exist when the timeout occurs.

What I think Codex should change

  • Terminate the whole snapshot process group on timeout and cancellation.
  • Add a regression test that forks a sleeping child and verifies that both processes are gone.

I have a cleanup tool and isolated Tokio reproducer, and can share either to validate a fix

zhangzhongnan928 · 22 days ago

Additional confirmation with process-group and stack evidence

I can confirm this is still reproducible on a newer Codex Desktop build, and the evidence strongly supports the process-group cleanup explanation in the previous comment.

Environment

  • Codex Desktop: 26.730.61639, build 6234
  • Embedded Codex runtime reported in local logs: 0.147.0
  • macOS 26.5.2 (25F84), arm64
  • zsh with NVM 0.39.0
  • App bundle on this installation: /Applications/ChatGPT.app, bundle id com.openai.codex

PID/PGID timeline

Two incidents were correlated from Codex's SQLite logs, macOS unified logs, ps, and lsof:

14:29:39.935  snapshot zsh group leader 50222 starts
14:29:42      descendant zsh 50933 starts in PGID 50222
14:29:49.923  codex_core::shell_snapshot logs:
                "Snapshot command timed out for zsh"
14:29:49.932  group leader 50222 exits

14:29:40.290  snapshot zsh group leader 50244 starts
14:29:42      descendant zsh 50934 starts in PGID 50244
14:29:50.276  second shell snapshot timeout
14:29:50.285  group leader 50244 exits

Afterward:

PID    PPID  PGID   STATE  CPU
50933  1     50222  R      ~94-100%
50934  1     50244  R      ~98-100%

The original group leaders no longer existed, but the surviving processes retained their dead leaders' PGIDs. Both had no TTY, stdin was /dev/null, and stdout/stderr were pipes whose reader ends no longer existed. The Codex backend process that launched the snapshots was still running, so reparenting did not require a full app exit or restart.

This looks like: the timeout kills the direct shell/group leader, a zsh descendant created during shell initialization survives in the same process group, and the kernel reparents that descendant to PID 1.

CPU stack

A 3-second native sample of one survivor collected 2,452 stack samples:

  • 2,208 samples (~90%) were in zsh execwhile
  • the stack included script sourcing, shell-function execution, command substitution (getoutput), and a shell while loop
  • 738 top frames were sigprocmask
  • 471 top frames were stat
  • lsof showed the process still had both .zshrc and .nvm/nvm.sh open
  • there were no samples in write, poll, kevent, functions, alias, or export

The hottest path is therefore a zsh loop during NVM initialization, before the snapshot reaches the functions/aliases/exports output phase. The strongest candidates are NVM's upward path-search functions such as nvm_find_project_dir or nvm_find_up, but the native stack does not expose the exact shell function name, so that part remains unconfirmed.

The closed output pipes appear to be a consequence of the timeout, not the cause of the CPU spin.

Recurrence and output-size evidence

At the time of inspection:

  • 58 zsh snapshot timeouts had been logged that day across 32 distinct thread IDs
  • 10 orphan snapshot zsh processes were simultaneously consuming roughly one core each
  • failures occurred across ordinary user tasks and internal/background tasks, not one specific session
  • other snapshots on the same machine succeeded in about 2 seconds and produced approximately 250 KB / 9,833 lines
  • manually sourcing the same .zshrc completed in about 5 seconds, and isolated functions, setopt, aliases, and exports checks all exited successfully

This argues against machine performance, snapshot output size, or an unconditional .zshrc infinite loop as the primary cause.

Interpretation

The NVM loop is an environment-dependent trigger for the 10-second timeout, but the persistent orphan/high-CPU behavior is a Codex lifecycle bug: timeout/cancellation cleans up the direct child but not all descendants in the snapshot process group.

A regression test that has the snapshot shell fork a busy or sleeping descendant, then verifies that both the leader and descendant are gone after timeout/cancellation, should cover this failure mode.