macOS Desktop: main thread spawns git ~50/s — uncached core.fsmonitor probe + per-file untracked diffs in repos with large untracked dirs

Open 💬 1 comment Opened Jul 18, 2026 by StarryKira

What version of the Codex App are you using (From "About Codex" dialog)?

26.715.31925 (5551) — Codex Framework 150.0.7871.124

What subscription do you have?

ChatGPT Pro 20x

What platform is your computer?

macOS 26.5.2 (25F84), Darwin 25.5.0, arm64 (Apple Silicon)

What issue are you seeing?

The Electron main process sustains 35–80% CPU indefinitely because the bundled VS Code git extension spawns git roughly 45–50 times per second, synchronously, on the main thread.

I traced this end-to-end. Root cause is a large untracked directory inside an opened repo — in my case a stray pnpm store (.pnpm-store/, 54,872 files, 649 MB) that was neither tracked nor gitignored.

1. sample shows the main thread blocked in process creation. ~21% of all main-thread samples sit directly in posix_spawn:

2145 __CFRunLoopRun
  ... node::worker::MessagePort::OnMessage
      ... _register_external_reference_process_wrap
          689 uv_spawn + 3108
            689 posix_spawn + 496
              660 __posix_spawn        <-- main thread, synchronous

Because the main thread is stuck in posix_spawn, it also reaps children late — ps shows a steady trail of <defunct> git processes.

2. What it actually runs. ps cannot read the argv (these processes live <1 ms), so I wrote a small native poller using proc_listpids() + sysctl(KERN_PROCARGS2). Over a 15 s capture, sampled by duty cycle:

| share of git process time | command |
|---|---|
| ~94% | git config --null --get core.fsmonitor |
| ~4% | git -c ... diff --no-ext-diff --no-textconv --no-index --raw --numstat -z -- /dev/null <file> |
| ~2% | git -c ... hash-object -- <file> |

All with CWD=<repo root>.

Two separate defects compound here:

  • (a) git config --null --get core.fsmonitor is re-forked before every single git invocation, uncached. It dominates by a wide margin — the app spends ~94% of its git time re-reading one static config key. This alone triples the process count of every git operation.
  • (b) Untracked files are diffed one file per process. For each untracked file the extension runs hash-object + diff --no-index against /dev/null to compute added-line stats. With 54,872 untracked files that is ~165,000 git process spawns queued up — hours of work at the observed rate.

3. System-wide collateral damage. Process creation at ~50/s saturates the macOS daemons that service it. Concurrent ps aux during the incident:

opendirectoryd   27.9%
WindowServer     19.0%
runningboardd     7.1%
launchd           6.5%
notifyd           6.5%
logd              6.3%
load average: 5.19 6.01 5.08

The system PID counter wrapped through its entire range (to 99999) during a routine observation window. So the true cost is well above what Activity Monitor attributes to Codex.

4. Newly added ignore rules do not take effect until restart. After adding .pnpm-store/ to .git/info/exclude, git status --porcelain -uall dropped from 54,897 → 24 — but CPU and spawn rate were unchanged (39.5% CPU, ~40 spawns/s), because the already-enumerated file list is held in memory and keeps draining. Only a full app restart clears it.

What steps can reproduce the bug?

  1. Open any git repo in Codex Desktop on macOS.
  2. Create a large untracked directory inside it that is not in .gitignore. A pnpm store reproduces it exactly:

``bash
cd <repo>
pnpm install --store-dir .pnpm-store # ~50k files
``
Any directory with tens of thousands of untracked files works.

  1. Leave the app open — no prompt or task required, it happens while idle.
  2. Observe:

``bash
# spawn rate (replace <PID> with the ChatGPT/Codex main process)
end=$((SECONDS+10)); while [ $SECONDS -lt $end ]; do \
ps -eo pid,ppid,comm | awk -v p=<PID> '$2==p && $3 ~ /git/ {print $1}'; \
done | sort -u | wc -l
``
I measured 400–479 distinct git PIDs per 10 s window, consistently across many runs.

  1. sample <PID> 5 confirms the main thread sitting in posix_spawn.

What is the expected behavior?

  • core.fsmonitor should be read once per repo and cached, not re-forked before every git call. This is the single highest-leverage fix — it removes ~94% of git process time on its own.
  • Untracked-file diff stats should be batched (one git invocation covering many paths) rather than 2 processes per file, and should be capped/skipped past a threshold (e.g. >5,000 untracked files), or deferred until the user actually expands that part of the SCM tree.
  • Process spawning must not happen synchronously on the Electron main thread — this blocks UI and delays child reaping.
  • Changes to .gitignore / .git/info/exclude should invalidate the pending scan queue without requiring an app restart.

Additional information

Workaround (both steps required): add the directory to .gitignore and restart the app — the ignore rule alone does not stop the in-flight queue. In my case I deleted the stray store outright, which dropped untracked files to 24.

Possibly related, but I believe none identify this mechanism:

  • #29374 — high CPU / overheating on Apple Silicon, no root cause identified. This may well be the underlying cause for that report.
  • #29110 — Windows: repeated git root/metadata probes while idle; also shows git config --null --get core.fsmonitor in the loop, which supports defect (a) being cross-platform.
  • #29408 — Windows: repeated/stuck git.exe polling in multi-repo workspaces.
  • #25159 — sustained main-process CPU burn after update.

Happy to supply the native argv-capture source or raw sample output if useful.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗