[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"

Status Fixed / completed
Maintainer reply None cached
Activity 1 comment · opened Sep 10, 2026 · closed Sep 21, 2026

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 <repo>/.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 <repo>/.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 <repo>/.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 <wt> in 28135ms (root 27939ms, workspaces=5, install_state=false)
2026-09-04 08:03:42 [info] [worktreeDepSeed] seeded for <wt> in 32876ms (root 32325ms, workspaces=5, install_state=false)
2026-09-04 12:10:49 [info] [worktreeDepSeed] seeded for <wt> in 26968ms (root 26457ms, workspaces=5, install_state=false)
2026-09-05 06:45:02 [info] [worktreeDepSeed] seeded for <wt> in 27267ms (root 27089ms, workspaces=5, install_state=false)
2026-09-08 06:51:53 [info] [worktreeDepSeed] seeded for <wt> in 33252ms (root 32927ms, workspaces=5, install_state=false)
2026-09-08 06:54:09 [info] [worktreeDepSeed] seeded for <wt> in 28447ms (root 28264ms, workspaces=5, install_state=false)
2026-09-08 07:56:51 [info] [worktreeDepSeed] seeded for <wt> in 30996ms (root 30536ms, workspaces=5, install_state=false)
2026-09-08 08:38:41 [info] [worktreeDepSeed] seeded for <wt> in 28850ms (root 28344ms, workspaces=5, install_state=false)
2026-09-08 08:39:33 [info] [worktreeDepSeed] seeded for <wt> in 28443ms (root 28182ms, workspaces=5, install_state=false)
2026-09-09 05:49:29 [info] [worktreeDepSeed] seeded for <wt> 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 <wt> in 31324ms (root 31103ms, workspaces=5, install_state=false)
2026-09-10 07:40:38 [info] [worktreeDepSeed] seeded for <wt> 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 <unknown binary>)   <-- 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.

View original on GitHub ↗

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