[BUG] Recursive FileChanged watchPaths registration over a large NFS directory blocks the main thread at startup (D state, rpc_wait_bit_killable)
Preflight Checklist
- [x] I have searched existing issues and this is not a duplicate. Related-but-distinct: #22041, #52995, #30302 concern large session transcripts; transcript size was tested and ruled out here (see "What's Wrong?").
- [x] This is a single bug report (the FileIndex finding from the same investigation is filed separately).
- [x] I am using the latest version of Claude Code (2.1.258; also reproduced on 2.1.233).
What's Wrong?
A SessionStart hook returns hookSpecificOutput.watchPaths pointing at the session's own folder on an NFS mount (/mnt/nfs-share/sessions/<name>/), so the session is woken when a file appears there. When that folder happened to contain a large subtree (~137,000 files / ~40,000 directories of hourly config snapshots), registering the watch put Claude Code's main thread into uninterruptible sleep on NFS RPCs during startup, contributing to a 20–45 second freeze before the prompt accepted input. The footer showed /rc connecting… throughout, which points the user at Remote Control; RC was merely queued behind the blocked loop.
Setup: multiple long-lived sessions sharing one ~/.claude on a shared home directory (Debian LXC); each session has a folder on an NFS mount that a SessionStart hook registers as a watch path. New sessions were unaffected because they have no name yet and therefore no watch path — which made this look like a resume/transcript-size problem. Transcript size was ruled out directly: re-creating the session under a new id with the same 16MB transcript did not change the freeze, and neither did removing per-session file-history/subagents/tool-results data, disabling Remote Control, or dropping --name.
What Should Happen?
Registering a watch path should not block the UI thread. Options: register watches asynchronously after the prompt is interactive; cap the recursive setup by file count or depth and fall back to a shallow (non-recursive) watch of the requested directory; or detect a network filesystem (nfs, cifs, fuse) and degrade to a shallow/polling watch with a debug-log notice. The hook contract only needs the folder root observed, so a shallow watch would satisfy the common case.
Error Messages/Logs
Debug log around the watch registration (2.1.258):
Hook SessionStart (bash <hook>) provided 1 watchPaths
FileChanged: watching 1 paths
Thread sampler reading /proc/<pid>/task/<pid>/stat and wchan every 250ms during the first 42s of a launch (main thread only):
samples: 170 | state counts: {'R': 97, 'D': 73} | top wchan: [('0', 106), ('rpc_wait_bit_killable', 64)]
main-thread cpu ticks over window: 84 -> 2384 (=23.0s CPU in 42s)
D-state runs (s from start): 2.5-2.5, 3.8-4.0, 4.5-4.5, 5.0-5.0, 6.0-6.3, 7.0-7.0, 8.0-9.3, 9.8-9.8, 10.3-10.3, 11.0-11.0, 11.5-12.0, 12.5-12.8 … total D time ≈ 18.3s
rpc_wait_bit_killable is the SUNRPC client wait used by the NFS client — the main thread was blocked on NFS round-trips, interleaved with CPU work. The same launch also logged [event-loop-stall] blocked for … lines totalling ~38s.
Size of the watched folder at the time (find … -type f | wc -l): 137,100 files, 40,471 directories, in a subtree of hourly snapshot directories (each ~500 files / ~200 dirs). Walking it with find over NFS took 2.2s; the watch registration cost far more.
Resolution (confirmed): moving that subtree out of the watched folder (leaving 90 files) removed the D-state waits; with the separate FileIndex issue also fixed, the same session now starts with zero event-loop stalls.
Steps to Reproduce
- Mount an NFS export and create a directory on it with a large subtree, e.g.
mkdir -p /mnt/nfs-share/sessions/test && cd /mnt/nfs-share/sessions/test && for i in $(seq 1 300); do mkdir -p snap$i/{a,b,c,d,e}; for d in a b c d e; do for j in $(seq 1 90); do : > snap$i/$d/f$j; done; done; done(~135k files). - Add a
SessionStarthook that returns that directory as a watch path:
``json`
{"hookSpecificOutput":{"hookEventName":"SessionStart","watchPaths":["/mnt/nfs-share/sessions/test"]}}
FileChanged` hook (any command) so the watch is used.
and a
- Launch
claude --debugfrom a normal cwd. Sample the main thread (cat /proc/<pid>/task/<pid>/stat /proc/<pid>/task/<pid>/wchanin a loop) during the first 30–60s; observeD/rpc_wait_bit_killableand delayed input acceptance. - Point the watch path at an empty directory (or move the subtree out) and relaunch: no D-state waits, immediate prompt.
Claude Model
Other — Fable 5.1; also seen on Opus 5. Model is irrelevant (before the first request).
Is this a regression?
I don't know — reproduced on 2.1.233 and 2.1.258, so not a 2.1.258 regression.
Last Working Version
n/a
Claude Code Version
2.1.258 (Claude Code) — native installer. Also 2.1.233.
Platform
Anthropic API (claude.ai subscription sign-in)
Operating System
Ubuntu/Debian Linux — Debian 13 in an LXC container, 8 cores; NFS client to a NAS export (1MB block size).
Terminal/Shell
bash; xterm.js-based terminal app over SSH, also a plain SSH terminal — no difference.
Additional context
- This is separate from, and stacked on top of, the FileIndex issue filed alongside it: on this machine both hit at once (index build ≈ 41s CPU on the main thread; watcher registration ≈ 18s of NFS waits interleaved). Fixing either alone left a noticeable freeze; fixing both removed it.
- The large subtree was our own doing (a backup job writing into the session folder), but the failure mode — main thread blocked on NFS during watch setup — applies to any user whose watch path lands on a big or slow network directory.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗