Repository navigation
Description
Related: #53416 (file watcher restart loop). I'm not certain this is the same bug — it may be a variant that only shows up on NixOS — but it's the same subsystem, and my symptoms are more severe than the restart-loop churn described there. Splitting it off if maintainers agree it has a distinct root cause.
Correction 2026-10-14: the original root-cause section attributed this to the watcher following .direnv symlinks. Further investigation showed the watcher never follows symlinks — it subscribes /nix/store directly as a parent-directory watch for home-manager-managed skill derivations. See the comment below for the evidence. The symptoms, numbers and reproduction are unchanged.
Summary
On NixOS, a freshly started opencode serve --service consumes ~523,000 of my 524,288 inotify watches (fs.inotify.max_user_watches) within seconds of starting. With the system-wide per-user budget exhausted, every other file-watching application on the machine fails with ENOSPC — which is inotify exhaustion, not disk space.
Environment
Linux framework-13 7.2.9 #1-NixOS SMP PREEMPT_DYNAMIC Sat Oct 3 10:43:16 UTC 2026 x86_64 GNU/Linux
NixOS 26.11.20261010.6510408 (Zokor)
opencode v2.0.27 (opencode2 from numtide/llm-agents.nix)
fs.inotify.max_user_watches = 524288
My ~/.config/opencode is managed by home-manager (config in modules/home/ai/ai-agents.nix of my dotfiles). This matters because of how home-manager deploys config files: it materializes everything under ~/.config as symlinks into the nix store, e.g.
~/.config/opencode/opencode.json -> /nix/store/vk3h8583hi3vd6qfm7m28gyslw17raxg-home-manager-files/.config/opencode/opencode.json
~/.config/opencode/themes/stylix.json -> /nix/store/f948xrwrc111fvkygc8dwbv8hhrgk9xs-opencode-stylix.json
What happens
A freshly started opencode serve --service accumulates ~523,226 inotify watches on a single inotify fd within seconds of starting (measured via /proc/<pid>/fdinfo/<fd>). Effects:
nh switch(nixos-rebuild wrapper) fails withnom: addWatch: resource exhausted (No space left on device)- new opencode TUI instances crash at startup:
Error: ENOSPC: no space left on device, watch '~/.config/opencode' - opencode's own log records the starvation:
level=ERROR message="location watcher subscription failed" path=/home/bene/github/britter/nix-configuration/.git/HEAD
cause="... Error: ENOSPC: no space left on device, watch '/home/bene/github/britter/nix-configuration/.git'"
Killing the service fixes everything; the next instance consumes the budget again immediately. Fully reproducible. Note the effect is system-wide: unrelated applications (nom, other TUIs) are the visible victims.
Root cause
The skill directories resolve to their real locations, which on home-manager-managed NixOS are top-level store entries: /nix/store/<hash>-hm_expert, /nix/store/<hash>-hm_tutor, etc. The service log shows the watcher subscribes each resolved skill directory, then immediately subscribes /nix/store itself:
watcher subscribe path=/nix/store/0dj3vjkvs055qlfibpi4x416hghp7g1g-hm_expert type=directory
watcher subscribe path=/nix/store/zda04fwpaa5kk36dykj6cl9qfaf3kx24-hm_expert type=directory
... (7 skill directories total)
watcher subscribe path=/nix/store type=directory
The /nix/store subscription looks like a parent-directory watch — watching the parent of a resolved directory so that its replacement or deletion is noticed, which is exactly what happens on home-manager switch. On NixOS, the parent of a top-level derivation is the store root itself, so this parent watch becomes a recursive watch over 1,416,513 directories (find /nix/store -type d | wc -l). That exceeds the 524,288 watch budget, so the watcher stops only when the kernel refuses.
On a normal filesystem the same logic resolves to ~/.config/opencode/skills/ — a few dozen entries — which is why this doesn't reproduce elsewhere.
Reproduction
- NixOS with home-manager managing
~/.config/opencode(skills resolving to top-level store derivations) opencode serve --servicegrep -c '^inotify wd' /proc/$(pgrep -f 'serve --service')/fdinfo/*reaches ~523k within seconds- Other applications' file watchers fail with ENOSPC
Workaround
OPENCODE_DISABLE_FILEWATCHER=true in the service environment stops it (see also #53417 for the confusion around this variable). Cost: no config hot-reload, which is acceptable under home-manager since config changes arrive via home-manager switch and sessions get restarted anyway.
Expected behavior
The watcher should not recursively expand parent-directory watches over trees with enormous entry counts, and should degrade gracefully on ENOSPC instead of starving every other file-watching process on the system. Candidates: skip the parent watch when the resolved path is already a top-level entry of a huge tree, refuse roots above some size threshold, or cap recursive expansion.
Activity
opencode-agent commentedon Oct 14, 2026
Thanks for the detailed report. I tried to reproduce this on v2.0.27 (
opencode serveon Linux) and couldn't get the watch count past about 16. Here's what I tried:.direnv/flake-inputs/<name>symlinked to a separate tree of ~5,000 directories, then requests to the server for that project (location, config, skills, file search, VCS status, sessions). The project directory only gets one-level, non-recursive watches, plus a single-file watch on.git/HEAD. Nothing went into the symlinked tree.~/.config/opencodeset up home-manager style, withopencode.jsonand twoskills/entries as symlinks to other directories. One of the skills pointed to a directory with aSKILL.mdplus a symlink to the big tree. The recursive watches on~/.config/opencode,skills/and the resolved skill directory did not follow symlinks into the big tree.So on 2.0.27 the watcher doesn't seem to follow symlinks in general. My guess is that on your machine one of the recursive watch roots resolves to a large store directory itself, for example a skill or plugin path pointing at a whole flake input source, or the
home-manager-filestree. I couldn't confirm that, though. These would help:watcher subscribe/watcher startedlines from~/.local/share/opencode/log/opencode.logfor one service start, especially those withtype=directoryandbackend=inotify, including the full paths (redact anything private).type=directorypath, roughly how many directories it contains (find -L <path> -type d | wc -landfind <path> -type d | wc -l). This shows whether the size comes from following symlinks or from the directory itself.opencode.json(pluginentries and any skill or instruction paths), and whatls -la ~/.config/opencode ~/.config/opencode/skillsshows.You can reply here or edit the issue. If you edit it, please also leave a comment so we know it's updated.
Related issues:
[-]server (NixOS): file watcher recursively watches /nix/store via home-manager and direnv symlinks, consuming ~523k inotify watches at startup and causing system-wide ENOSPC[/-][+]server (NixOS): watcher subscribes /nix/store as parent-directory watch for home-manager skill derivations, consuming ~523k inotify watches and causing system-wide ENOSPC[/+]britter commentedon Oct 14, 2026
Correction and follow-up data as requested (@opencode-agent). I've also edited the issue body — the
.direnv/flake-inputstheory in the original report was wrong. The watcher never follows symlinks; it subscribes/nix/storedirectly as a parent-directory watch for the resolved skill directories. That also explains why the repro attempts stayed at ~16 watches.1. Full
watcher subscribe/watcher startedlines for one service start (run=f678edfb, 2026-10-14T08:15:16Z,http.spanfields stripped). The same pattern appears in every service start and on every config-reload re-subscribe:The
/nix/storesubscription was logged 15 times today — once per service start and again on every config-reload re-subscribe (the #53393 loop).2. Directory counts.
/nix/storecontains 1,416,513 directories (find /nix/store -type d | wc -l;find -Lgives the same — the store has essentially no symlinked directories). Everything else subscribed is small:~/.config/opencode≈ 4k entries, project trees in the tens of thousands, the individualhm_*skill dirs are tiny. Notably, the subscribed skill directories are top-level store entries, so their common parent is the store root — which is what I believe the/nix/storesubscription is: a parent-directory watch, deduplicated across all seven skills. On a normal filesystem the same watch would land on~/.config/opencode/skills/, a few dozen entries.3. Rendered config and directory state.
(No
~/.config/agentsdirectory currently exists on disk, incidentally — those five instruction paths point at hm-files entries that this config generation no longer deploys. Possibly related to the resubscribe loop.)One hypothesis I can't confirm without reading the watcher source: the skill loader resolves each skill directory to its real path (
hm_expertetc. in the log), and the watcher adds a parent-directory watch on the resolved path so replacement/deletion is noticed. On NixOS the parent of a top-level derivation is the store root, so that parent watch becomes a recursive watch on 1.4M directories. This started today, immediately after ahome-manager switchthat replaced the skills — before that, the skill paths presumably resolved somewhere harmless.The ENOSPC numbers from the report stand unchanged: 523,226 of 524,288 watches consumed within seconds of service start.
opencode-agent commentedon Oct 14, 2026
Thanks for the corrected analysis and the logs: the
/nix/store type=directoryline was the missing piece. I can confirm it on v2.0.27, and it still happens on the latestdev(34d8ded).The script below reproduces it without Nix:
/nix/store, with a skill file at its top level~/.config/opencode/skills/nix.expert/is a plain directory whoseSKILL.mdis a symlink to that file, the way home-manager deploys itopencode serveis started and the skills are requested onceThe server then logs
watcher subscribe path=<store> type=directoryand holds about 5,060 inotify watches, one per directory of the unrelated tree. On a real store with 1.4M directories, that is enough to use upmax_user_watches.The cause matches your hypothesis. The skill loader resolves each
SKILL.mdto its real path. If that path is outside the directories it already watches, it adds a recursive watch on the file's parent directory, which here is the store root.OPENCODE_DISABLE_FILEWATCHER=trueis still the workaround for now.Reproduction script (repro.sh)
Run it:
bash repro.shbritter commentedon Oct 14, 2026
Workaround for other NixOS/home-manager users: wrap the opencode package so the service inherits
OPENCODE_DISABLE_FILEWATCHER=true(see also #53417 for this variable's documentation gap).With home-manager, wrap the package instead of setting the variable in your shell — the background service (
serve --service) is spawned by the CLI, so it only picks up the variable if the package itself carries it:Setting the variable in
home.sessionVariablesis not sufficient: the managed service is (re)spawned by whatever CLI instance wins the service election, and its environment may come from a different, non-interactive context.With this in place, the service holds ~111 inotify watches at startup instead of ~523k, and system-wide
ENOSPCfailures (nh switch, new TUI instances) stop immediately. Cost: no config/skill hot-reload — changes require restarting opencode sessions, which is acceptable when config arrives viahome-manager switchanyway.ai-agents: disable opencode file watching
Dante-dan commentedon Oct 14, 2026
I’m taking this on. The fix watches an individually symlinked skill’s resolved file through the existing non-recursive file watcher, while keeping recursive discovery for directory symlinks. The skill tests pass, including target replacement, link retargeting, and discovering new skills behind a directory symlink.