anthropics / anthropics/claude-code
[BUG] macOS Desktop: worktreeDepSeed's 680k-file clonefile starves a synchronous main-thread rename(2) — deterministic ~30 s whole-app freeze per new worktree session, discarded as "likely sleep"
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Preflight Checklist
- [x] I have searched existing issues. No issue mentions `worktreeDepSeed` or worktree dependency seeding. Nearest neighbours, both distinct and separated under "Related" below: #92325 (the sleep-heuristic accounting bug that hides *this* stall from telemetry) and #92785 (macOS desktop freezes on a similar cadence, but attributed to an EventEmitter leak — ruled out here with logs).
- [x] This is a single bug report.
- [x] I am using the latest version (Desktop 1.49585.0).
### What's Wrong?
Opening a new worktree-backed session in the Desktop Code tab blocks the **Electron main process for 26–38 seconds**, beachballing the entire app — every window, every session, not just the new one.
A `sample(1)` captured *during* a stall (condensed under **Error Messages/Logs** below) shows the mechanism is two overlapping pieces of the app's own work:
| thread | samples | doing |
|---|---|---|
| `com.apple.main-thread` | **20,683 / 21,070 (98.2%)** | blocked in a single synchronous `rename(2)` -> `__rename` |
| `tokio-runtime-worker` | **21,070 / 21,070 (100%)** | one recursive `clonefileat(2)` spanning the whole window |
The `node_modules` clone itself is correctly off the main thread. The problem is that the **main thread makes a blocking `rename(2)` while that clone is in flight**, and the clone -- a recursive in-kernel clone of **6.5 GB / 680,634 files** -- holds APFS metadata locks long enough to stretch that normally sub-millisecond `rename` to ~20.7 seconds. The main-thread call arrives from app JS (`CFRunLoop` source0 -> `node::InternalMakeCallback` -> JIT frames -> an fs binding); the JIT frames are unsymbolized, so the sample cannot name the renamed path.
Neither half freezes the app on its own. A sync fs call on the UI thread is invisible under normal I/O; the app only makes it a 30-second freeze by starving itself with its own clone.
This is not intermittent. **29 of 29 seed events in my logs took over 26 seconds:**
| | ms |
|---|---|
| n | 29 |
| min | 26,360 |
| median | 28,370 |
| max | 38,069 |
| mean | 29,455 |
One new worktree session = one guaranteed ~30 s whole-app freeze. In normal use that lands every 5–10 minutes, and it is hitting every engineer on my team who works in this repo.
**Three freezes from one morning, each with the stall and the seed emitted in the same second:**
```
2026-09-10 07:39:07 [warn] [event-loop-stall] main process blocked for 30202ms [likely sleep: duration_heuristic] (task WindowControl.reportSidebarPlaceholder=30285ms, total 8, cumulative 20066ms, rss 465MB)
2026-09-10 07:39:07 [info] [worktreeDepSeed] seeded for /.claude/worktrees/worktree-A in 31324ms (root 31103ms, workspaces=5, install_state=false)
2026-09-10 07:40:38 [warn] [event-loop-stall] main process blocked for 36850ms [likely sleep: duration_heuristic] (task WindowControl.reportSidebarPlaceholder=36857ms, total 8, cumulative 20066ms, rss 459MB)
2026-09-10 07:40:38 [info] [worktreeDepSeed] seeded for /.claude/worktrees/worktree-B in 38069ms (root 37674ms, workspaces=5, install_state=false)
2026-09-10 10:24:55 [warn] [event-loop-stall] main process blocked for 29114ms [likely sleep: duration_heuristic] (task WindowControl.reportSidebarPlaceholder=29111ms, total 9, cumulative 23736ms, rss 347MB)
2026-09-10 10:24:56 [info] [worktreeDepSeed] seeded for /.claude/worktrees/worktree-C in 30302ms (root 29891ms, workspaces=5, install_state=false)
```
The `sample(1)` covers that last one: sampling started at 10:24:25.924, one second after the worktree directory appeared, and ran 25 s -- inside the 29,114 ms stall that ended at 10:24:55.
The `root NNNNNms` figure is the root-tree clone alone; it brackets the stall almost exactly, which is the correlation the sample then explains.
**Secondary defect — this stall is invisible to your telemetry.** Both 30 s+ stalls above are tagged `[likely sleep: duration_heuristic]`, and a tagged stall increments neither `total` nor `cumulative`. Watch the counters across five consecutive stalls:
| time | blocked | tagged as sleep? | total | cumulative |
|---|---|---|---|---|
| 05:30:58 | 25,406 ms | yes | 6 | 17,978 ms |
| 06:26:15 | 1,504 ms | no | 7 | 19,482 ms |
| 06:54:20 | 584 ms | no | 8 | 20,066 ms |
| 07:39:07 | **30,202 ms** | yes | **8** | **20,066 ms** |
| 07:40:38 | **36,850 ms** | yes | **8** | **20,066 ms** |
A 584 ms stall is counted. Stalls of 30 s, 37 s and 29 s are not. The machine did not sleep — I was typing in the app when both fired, and the app logs an explicit `[event-loop-stall] OS resume` line on real wake (present at 05:21:22, absent at both 07:39 and 07:40). This is #92325 on macOS, and it is why a fully deterministic 30-second freeze has presumably never shown up in aggregate stall data.
### What Should Happen?
The main thread should not make blocking fs calls, and certainly not while the app is saturating the same volume. In rough order of cost to fix:
1. Make the main-thread `rename` async (or move it to a worker). This alone removes the freeze -- the clone can keep running as-is.
2. Failing that, do not overlap main-thread fs work with the seed clone: hold the rename until the seed's `clonefileat` completes.
3. Consider skipping or chunking the seed above a size/file-count threshold -- one recursive clone of 680k files is a very long metadata-lock hold regardless of which thread issues it.
4. Expose an opt-out setting for repos where the clone costs more than it saves.
Separately, the `duration_heuristic` sleep test needs a real corroborating signal (an `OS resume` event, a wall-clock-vs-monotonic delta) rather than duration alone, so stalls like this stop being discarded.
### Error Messages/Logs
Every seed event on this machine (`~/Library/Logs/Claude/main*.log`), showing that none completes in under 26 s:
```
2026-09-04 08:03:10 [info] [worktreeDepSeed] seeded for in 28135ms (root 27939ms, workspaces=5, install_state=false)
2026-09-04 08:03:42 [info] [worktreeDepSeed] seeded for in 32876ms (root 32325ms, workspaces=5, install_state=false)
2026-09-04 12:10:49 [info] [worktreeDepSeed] seeded for in 26968ms (root 26457ms, workspaces=5, install_state=false)
2026-09-05 06:45:02 [info] [worktreeDepSeed] seeded for in 27267ms (root 27089ms, workspaces=5, install_state=false)
2026-09-08 06:51:53 [info] [worktreeDepSeed] seeded for in 33252ms (root 32927ms, workspaces=5, install_state=false)
2026-09-08 06:54:09 [info] [worktreeDepSeed] seeded for in 28447ms (root 28264ms, workspaces=5, install_state=false)
2026-09-08 07:56:51 [info] [worktreeDepSeed] seeded for in 30996ms (root 30536ms, workspaces=5, install_state=false)
2026-09-08 08:38:41 [info] [worktreeDepSeed] seeded for in 28850ms (root 28344ms, workspaces=5, install_state=false)
2026-09-08 08:39:33 [info] [worktreeDepSeed] seeded for in 28443ms (root 28182ms, workspaces=5, install_state=false)
2026-09-09 05:49:29 [info] [worktreeDepSeed] seeded for in 26599ms (root 26301ms, workspaces=5, install_state=false)
... 13 more between 2026-09-09 05:51 and 2026-09-10 06:22, all 26.3-37.7s ...
2026-09-10 07:39:07 [info] [worktreeDepSeed] seeded for in 31324ms (root 31103ms, workspaces=5, install_state=false)
2026-09-10 07:40:38 [info] [worktreeDepSeed] seeded for in 38069ms (root 37674ms, workspaces=5, install_state=false)
```
Process-memory sample taken at a stall, showing the app is not memory-starved — this is pure main-thread blocking:
```
2026-09-10 06:26:16 [info] [process-memory] trigger=stall tree_rss_sum=3895MB tree_footprint_sum=3463MB electron(14)=2007MB children(38)=1888MB top=[electron_renderer:684:674MB electron_main:598:445MB ...] sys_free=5867MB/32768MB
```
Condensed main-thread stack from that sample (21,070 samples total; leaf held 20,683 of them). Frame symbolization in the Electron Framework is nearest-symbol noise — the reliable frames are the AppKit/CoreFoundation ones at the top and the libsystem_kernel ones at the leaf:
```
21070 Thread_7528979 DispatchQueue_1: com.apple.main-thread (serial)
21070 ElectronMain (in Electron Framework)
...
21070 -[NSApplication run] (in AppKit)
21070 -[NSApplication _nextEventMatchingEventMask:untilDate:inMode:dequeue:] (in AppKit)
21065 _DPSNextEvent (in AppKit)
21065 ReceiveNextEventCommon (in HIToolbox)
21059 _CFRunLoopRunSpecificWithOptions (in CoreFoundation)
20685 __CFRunLoopRun (in CoreFoundation)
20685 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
...
20684 node::InternalMakeCallback(...) (in Electron Framework)
20684 v8::Function::Call(...) (in Electron Framework)
20683 ??? (in ) <-- app JS, JIT, unsymbolized
...
20683 rename (in libsystem_kernel.dylib) + 16
20683 __rename (in libsystem_kernel.dylib) + 8
```
And, concurrently, on a separate thread for the entire window:
```
21070 Thread_8971372: tokio-runtime-worker
...
21070 clonefileat (in libsystem_kernel.dylib) + 8
```
The full 372 KB `sample` output contains no user paths, no repository or branch names, and no credentials (it is stacks plus a binary-images list, all under `/Applications/Claude.app`, `/usr` and `/System`). Happy to attach it or send it to a triage address on request.
### Steps to Reproduce
1. Use a monorepo with a large, gitignored `node_modules` (here: 6 bun workspaces; root `node_modules` 6.5 GB / 680,634 files, each workspace tree additionally seeded).
2. In the Desktop Code tab, start a new session with a worktree (any flow that leases a fresh worktree).
3. Immediately try to interact with **any** part of the app — a different session, the sidebar, the window controls.
4. The whole app beachballs for 26–38 s. It recovers on its own.
5. `grep -ahE "event-loop-stall|worktreeDepSeed" ~/Library/Logs/Claude/main*.log` — the stall and the seed appear in the same second, and the stall carries `[likely sleep: duration_heuristic]`.
Reproduces on every attempt, 29/29.
### Claude Model
Opus 5 (not model-dependent — the stall is in the desktop main process, before any model call)
### Is this a regression?
Yes
### Last Working Version
`worktreeDepSeed` does not appear anywhere in my rotated logs before **2026-09-04** (`main2.log`, `main3.log`, `main4.log`, covering 2026-08-12 → 2026-09-04: zero occurrences). First event 2026-09-04 08:03:10; volume steps up sharply from 2026-09-08 (23 events in `main.log`). So the feature landed around Sep 4 and the freezes date from there.
### Claude Code Version
Desktop 1.49585.0 (Electron 44.2.0), bundled CLI
### Platform
Anthropic API
### Operating System
macOS 26.6.2 (25G83), Apple M2 Max (Mac14,6), 32 GB RAM
### Terminal/Shell
Desktop app Code tab (zsh)
### Additional Information
- 24 live worktrees under `.claude/worktrees/` at time of report, so this fires many times a day.
- `install_state=false` on every event — whatever the install-state seeding step is, it is not contributing; the entire cost is the tree clone.
- From the bundle, the seed path is `sessions/worktreeDepSeed.ts` → `native.cloneAt` (APFS `clonefileat`), dispatched to a tokio worker, with outcomes `seeded | skipped_dest_exists | skipped_not_ignored | skipped_clone_unsupported | skipped_aborted | failed`. The copy-on-write blocks are cheap; the 680k-entry metadata work is not, and it is what starves the main thread's `rename`.
- I have not identified which path the main thread renames -- the calling JS frames are JIT and unsymbolized. `fs_usage -f filesys` during a seed would name it; happy to capture that if useful.
- The blocking task is consistently reported as `WindowControl.reportSidebarPlaceholder`, which looks like whatever was queued behind the seed rather than the cause.
### Related
- **#92325** — same `duration_heuristic` accounting bug, reported on Windows. This report is the macOS instance, and shows what the discarded stalls actually were.
- **#92785** — macOS desktop freezing on a similar 7–10 minute cadence, attributed to an EventEmitter listener leak. Ruled out on my machine: 110 `MaxListenersExceededWarning` lines total, most recent 2026-09-09 06:37, none within 25 hours of either freeze above.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the mentioned seed path, sessions/worktreeDepSeed.ts -> native.cloneAt, and reproduce from a large worktree-backed Desktop Code session while watching ~/Library/Logs/Claude/main*.log for worktreeDepSeed and event-loop-stall. The missing piece is the main-thread rename path; the report suggests fs_usage -f filesys during a seed. Done means new worktree seeding no longer freezes the whole app and long stalls are not discarded as likely sleep without corroboration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, macos, node.js, typescript
- Domain
- desktop, observability, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100