anthropics / anthropics/claude-code
[BUG] Desktop app re-runs `git ls-files --others :/` ~2.2×/sec without single-flighting, burning ~3 cores to find 3 untracked files (macOS, large monorepo)
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Summary
On macOS, the desktop app's diff/changes pane repeatedly spawns:
```
git ... -c core.fsmonitor=false ls-files --others --exclude-standard --full-name :/
```
There is no single-flight guard: a new scan is started every ~0.45s regardless of whether the previous one has finished. On a large monorepo each scan needs **~1.5s of CPU** and **~17s of wall time**, so they accumulate. I measured **32–51 concurrent** `git` processes and a **load average of 265** on a 10-core M2 Pro, with the app otherwise idle — no prompt running, no build, no user input.
The scan returns **3 untracked files**. It is re-run roughly **190,000 times/day** to rediscover the same 3 paths.
## Environment
| | |
|---|---|
| Claude desktop app | 2.110.1 |
| OS | macOS 15.5, arm64 |
| Hardware | Mac14,9 (M2 Pro, 10 cores) |
| git | 2.39.5 (Apple Git-154) |
| Repo | monorepo, 70 submodules, 26 GB `.git` |
## Measurements
All taken with the app idle (no active prompt, no build running).
**Spawn rate** — new PIDs matching the scan over a 10s window:
```
new ls-files PIDs in 10s: 22 => 2.20/sec
concurrent now: 32
```
**Pile-up** — age distribution of live scans. One to three processes alive at *every* second from 0 to 17, i.e. they are started far faster than they drain:
```
1 00:00 2 00:05 2 00:10 3 00:15
3 00:01 3 00:06 2 00:11 3 00:16
2 00:02 2 00:07 3 00:12 1 00:17
2 00:03 3 00:08 3 00:13
2 00:04 3 00:09 2 00:14
```
**Cost per scan** — three consecutive runs of the exact command the app runs:
```
run1 user 0.28 sys 1.33
run2 user 0.28 sys 1.23
run3 user 0.28 sys 1.16
```
~1.5s CPU each, dominated by `sys` (directory traversal syscalls).
**Output size:**
```
$ git ls-files --others --exclude-standard --full-name :/ | wc -l
3
```
**Resulting system state:**
```
load average: 265.05, 260.83, 258.99
git process count: 58
total %CPU across all processes: 529.8
```
## The arithmetic
2.2 scans/sec × ~1.5s CPU per scan ≈ **3.3 cores consumed continuously**, on a 10-core machine, to discover 3 files. This is independent of contention — it is CPU time, not wall time.
The wall-clock inflation to ~17s per scan is then self-amplifying: the scans contend with each other for filesystem I/O, which makes each slower, which increases how many are alive at once.
## Functional consequence
The call site passes a 30s timeout and returns `[]` on timeout:
```js
try {
i = await e.run(["ls-files","--others","--exclude-standard","--full-name",":/"], t, 3e4)
} catch { return [] }
```
At 17s and climbing, this is approaching its own timeout. Once crossed, the pane silently shows **no** untracked files while still burning the same CPU — the feature degrades to nothing but keeps the full cost.
## Call site
`app.asar` offset 2063953, duplicated at 3994157 (minified; names are mangled):
```js
async function Gi(e, t, n, r) {
let i;
try {
i = await e.run(["ls-files","--others","--exclude-standard","--full-name",":/"], t, 3e4)
} catch { return [] }
let a = i.split("\n").filter(Boolean);
if (a.length === 0) return [];
let o = a.slice(0, Si); // file-count cap
let s = { bytes: r }; // byte budget
let c = (await Ni(o, e.workingFileReadConcurrency, t => $i(e, n, t, s))) ...
```
The preceding function is a unified-diff parser, so this appears to feed the diff/changes view. The need for `ls-files --others` is understood: `git diff` cannot represent untracked files, so they must be listed and read separately to synthesize an added-lines diff.
Two observations about the caps: `slice(0, Si)` and the byte budget bound how many files are **read**, but `git ls-files` has already walked the whole tree by then — the walk itself is unbounded. And `:/` forces the scan from the repo root regardless of the user's working directory, so it always covers all 70 submodule paths.
## Things I ruled out
To preempt the obvious suggestions — none of these help, the walk cost is inherent:
- **`core.fsmonitor`.** The app passes `-c core.fsmonitor=false`. I understand why: `core.fsmonitor` accepts a *path to an executable hook*, so disabling it belongs with the other ACE guards the app sets (`core.hooksPath=/dev/null`, `core.askPass=`, `protocol.ext.allow=never`, `core.sshCommand=false`, `safe.bareRepository=explicit`, `credential.helper=`, plus `GIT_CONFIG_*` scrubbing). **But it is not the cause here.** With a healthy `git fsmonitor--daemon` already running for 23h, `fsmonitor=false` cost 16.8s and `fsmonitor=true` cost 19.5s — no improvement. `ls-files --others` does not benefit from it.
- **`core.untrackedCache`.** Already `true` in this repo, along with `feature.manyFiles=true`. No effect on this command.
- **`git status --porcelain=v2 -uall` instead** (which does use the untracked cache): `user 0.29 / sys 1.23` — identical cost. Not a fix.
So the problem is not *which* git command is used or how it is configured. It is that a ~1.5s-CPU operation is issued 2.2 times per second with no coalescing.
## Suggested fixes
In rough order of value:
1. **Single-flight the scan.** If one is in flight, don't start another. This alone would cut ~32 concurrent processes to 1 and is the smallest possible change.
2. **Adapt the interval to measured cost.** After a scan takes N ms, wait at least some multiple of N before the next. A scan that costs 1.5s of CPU should not be repeated every 0.45s.
3. **Make it event-driven.** Use a filesystem watcher and rescan on change, rather than polling. The app already depends on change detection elsewhere.
4. **Gate on visibility.** Only refresh when the diff/changes pane is actually open and focused.
## Related
[#94478](https://github.com/anthropics/claude-code/issues/94478) reports the same underlying behavior on Windows (~15–20 git spawns/sec, ~2M/day) and attributes it to a ~60ms poll interval. This report adds the macOS case, the per-scan CPU cost, the pile-up mechanism (scan duration far exceeding the respawn interval), the approaching-timeout failure mode, and evidence that git-side configuration cannot mitigate it.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by locating the duplicated minified call site in app.asar at offsets 2063953 and 3994157, then trace function Gi into the diff/changes refresh path. Reproduce the repeated git ls-files scan on a large repository and verify that the completed change prevents overlapping scans while still reporting untracked files without approaching the timeout.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, javascript
- Domain
- desktop, devtools, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100