macOS Desktop: main thread spawns git ~50/s — uncached core.fsmonitor probe + per-file untracked diffs in repos with large untracked dirs
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.fsmonitoris 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-indexagainst/dev/nullto compute added-line stats. With 54,872 untracked files that is ~165,000gitprocess 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?
- Open any git repo in Codex Desktop on macOS.
- 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.
- Leave the app open — no prompt or task required, it happens while idle.
- 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.
sample <PID> 5confirms the main thread sitting inposix_spawn.
What is the expected behavior?
core.fsmonitorshould 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
gitinvocation 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/excludeshould 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.fsmonitorin the loop, which supports defect (a) being cross-platform. - #29408 — Windows: repeated/stuck
git.exepolling 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗