fix(cc-watcher): stop burning 65k inotify watches on the plugin cache

`plugins` sat in WATCH_SUBDIRS, so it got fs.watch({recursive:true}).
On Linux that's a userland tree walk costing ONE inotify watch per
directory, and ~/.claude/plugins is the plugin cache: full git checkouts
with node_modules, 44k+ directories. The dashboard process held 65,464
watches against the default fs.inotify.max_user_watches of 65536,
leaving none for systemd/IBus — IBus exited on boot and Unikey died
with it.

Config Explorer only reads two top-level files out of that tree
(installed_plugins.json, known_marketplaces.json), so watch plugins/
flat: 1 watch instead of 44k. The small config subdirs stay recursive.

Measured on the running service: 65,464 -> 96 watches, and touching
installed_plugins.json still broadcasts cc_config_changed.
This commit is contained in:
2026-09-07 08:32:19 +07:00
parent 3223c8edac
commit ae9b3dbcd5
2 changed files with 89 additions and 3 deletions
+16 -3
View File
@@ -65,7 +65,18 @@ const IGNORED_PREFIXES = [
// never the whole of ~/.claude/, so the recursive watcher never registers
// interest in high-churn dirs (backups/, projects/, logs/) whose transient
// files crash Node's Linux userland recursive watcher mid-stat.
const WATCH_SUBDIRS = ["agents", "commands", "skills", "output-styles", "hooks", "plugins"];
const WATCH_SUBDIRS = ["agents", "commands", "skills", "output-styles", "hooks"];
// Config surfaces that are directories but are watched NON-recursively, because
// their tree is enormous and none of it is config. `plugins/` holds the plugin
// cache — full git checkouts with `node_modules/` (44k+ directories here). On
// Linux, Node's recursive fs.watch is a userland walk that registers ONE inotify
// watch per directory, so watching it recursively burned ~65k of the default
// 65536 `fs.inotify.max_user_watches` and starved the rest of the desktop
// (systemd, IBus/Unikey) of watches. Config Explorer only reads two top-level
// files from it (`installed_plugins.json`, `known_marketplaces.json`, see
// cc-discovery.js), both caught by a flat watch costing exactly one watch.
const WATCH_SUBDIRS_FLAT = ["plugins"];
let started = false;
let timer = null;
@@ -120,11 +131,13 @@ function safeWatchHome({ home, broadcast }) {
// Recursively watch ONLY the relevant config subdirs (never backups/, projects/,
// logs/, …) so nested changes still fire without watching the high-churn trees.
for (const sub of WATCH_SUBDIRS) {
// WATCH_SUBDIRS_FLAT get the same treatment minus recursion — see above.
for (const sub of [...WATCH_SUBDIRS, ...WATCH_SUBDIRS_FLAT]) {
const dir = path.join(home, sub);
const recursive = WATCH_SUBDIRS.includes(sub);
try {
if (!fs.existsSync(dir)) continue;
const w = fs.watch(dir, { recursive: true }, (_event, filename) => {
const w = fs.watch(dir, { recursive }, (_event, filename) => {
const full = filename ? path.join(dir, filename) : dir;
if (!isRelevantUnderHome(home, full)) return;
scheduleEmit(broadcast, full);