anthropics / anthropics/claude-code
[BUG] Recursive FileChanged watchPaths registration over a large NFS directory blocks the main thread at startup (D state, rpc_wait_bit_killable)
- Langage dominant
- Python
- Étoiles
- 145k
- Forks
- 23.1k
- Métriques de merge des PR
- Métriques de PR en attente
Description
### 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//`), 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 ) provided 1 watchPaths
FileChanged: watching 1 paths
```
Thread sampler reading `/proc//task//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
1. 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).
2. Add a `SessionStart` hook that returns that directory as a watch path:
```json
{"hookSpecificOutput":{"hookEventName":"SessionStart","watchPaths":["/mnt/nfs-share/sessions/test"]}}
```
and a `FileChanged` hook (any command) so the watch is used.
3. Launch `claude --debug` from a normal cwd. Sample the main thread (`cat /proc//task//stat /proc//task//wchan` in a loop) during the first 30–60s; observe `D` / `rpc_wait_bit_killable` and delayed input acceptance.
4. 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.
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Piste de recherche
Start at the SessionStart hook's watchPaths handling and FileChanged registration path, using the supplied large-tree NFS reproduction with --debug; compare it with an empty directory and inspect the main-thread stall logs. Done means startup reaches an interactive prompt without blocking the UI thread while the requested watch behavior remains functional.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- linux, python
- Domaine
- devtools, operating-systems, performance
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Active
- Clarté
- Plutôt claire
- Accessibilité débutants
- 55/100