[BUG] Settings-file watcher enumerates its parent directory, with no bound on parent size
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Claude Code 2.1.224, macOS 26.5.2 (Darwin 25.5.0), 18-core Apple Silicon
Summary
Claude Code watches ~/.claude/settings.json by watching the file's parent
directory. When that file is a symlink whose target lives in a very large
directory, Claude repeatedly opendirs and enumerates the whole directory.
On any Nix system this is the default situation: home-manager materialises~/.claude/settings.json as a symlink to a top-level /nix/store path, so the
parent directory is the Nix store itself — 366,845 entries on this machine.
Result: an idle Claude session burns 300–500% CPU for the entire duration of
any unrelated Nix build, and starves the build it is reacting to.
Impact
This is not a niche configuration. Every Nix/home-manager user who manages
Claude Code declaratively gets a symlinked settings.json by default, and anynix build anywhere on the machine triggers it. The two workloads then fight
for cores: the build crawls, and the idle assistant pins the box.
What Should Happen?
Suggested fixes
Any one of these would resolve it:
- Watch the file path itself rather than scanning the resolved target's parent
- If a parent-directory watch is required, do not enumerate the directory to
detect the change
- Bound it defensively: skip directory enumeration above some entry count, and
fall back to polling stat on the single file.
- Debounce: currently a burst of directory writes yields repeated full
enumerations, roughly one per second, with no coalescing. At the very least don't re-enter a scan while the previous one is still running
The last point matters independently of the symlink case: a full re-enumeration
per change event is expensive for any large config directory.
Error Messages/Logs
Steps to Reproduce
Evidence
Captured with sample(1) and fs_usage(1) against an idle session (no user
input) while nh darwin build . ran in an unrelated directory.
Hot leaf frames — a directory walk, not event delivery:
lstat 29316
__getdirentries64 14675
kevent64 6762 <- comparatively idle
fs_usage, 45s window:
| metric | value |
|---|---|
| accesses to the settings store path | 146 (~3/sec) |
| openat on /nix/store root | 16 |
| lstat on store entries | 741,519 |
| complete passes over 366,845 entries | ~2.0 |
Each open is immediately followed by a getdirentries64 burst on the same fd:
17:01:15.725430 openat F=20 (R______________) [-2]//nix/store
17:01:15.726105 getdirentries64 F=20 B=0x1fc0
17:01:15.735722 getdirentries64 F=20 B=0x1fd8
17:01:15.736100 getdirentries64 F=20 B=0x1fc0
...
Entries are visited in unsorted readdir order and include .drv files and
source tarballs, i.e. the whole store directory is enumerated with no filtering.
Reproduction
- On macOS, make
~/.claude/settings.jsona symlink to a file that sits in a
directory with a very large number of entries:
````
ln -s /nix/store/<hash>-claude-code-settings.json ~/.claude/settings.json
(Any large directory reproduces it; the Nix store is simply the obvious case.)
- Start Claude Code and leave it idle.
- Cause churn in that directory — on Nix, any
nix buildthat fetches paths. - Observe:
sample <claude-pid> 10showslstat/__getdirentries64dominant;
sudo fs_usage -w -f filesys <claude-pid> shows repeated openat +
getdirentries64 on the large directory.
Control: replace the symlink with a real file, or point it at a file nested
inside a small directory, and the behaviour disappears completely — /nix/store
syscalls go from 215,483 in a 20s window to 0, with the build still running.
Claude Model
_No response_
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.224 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
_No response_
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗