Skip to content

watcher: fs watcher subscribes/unsubscribes skill directories in a tight loop (100%+ CPU, macOS) #53811

Open

Description

@bbruen3

Summary

On macOS, an opencode serve process (v2.0.21, also observed on v2.0.24) enters a tight, self-sustaining subscribe/unsubscribe loop in the native filesystem watcher, pegging a core and more (~250–320% CPU). The churn is entirely @parcel/watcher's FSEvents backend re-subscribing over skill directories — with zero filesystem changes, so it is not driven by any file activity. Setting OPENCODE_DISABLE_FILEWATCHER=1 stops it instantly.

Environment

  • opencode version: 2.0.21 (.opencode/bin/opencode, launched by OpenChamber) — also reproduced on 2.0.24 (homebrew opencode serve --service)
  • OS: macOS 26.6.2 (25G83) / Darwin 25.6.0, arm64
  • Terminal: desktop app (OpenChamber); TERM=xterm-256color, TERM_PROGRAM empty
  • Shell: /bin/zsh
  • Install/channel: release/latest
  • Active plugins: OpenChamber injects -opencode.browser (disabled) + /Users/<user>/.config/openchamber/agent-tool/openchamber-agent-tool; global opencode.json plugin is unset.

Reproduction

  1. On macOS, have several skill roots present, e.g. ~/.claude/skills, ~/.agents/skills, and project-level <project>/.claude/skills + <project>/.agents/skills.
  2. Start a server that boots locations for those directories, e.g. opencode serve --hostname 127.0.0.1 --port <p> with cwd $HOME (OpenChamber launches it with OPENCHAMBER_OPENCODE_CWD=$HOME), and open a project.
  3. Watch CPU: top/ps shows the process climbing to ~250–320%.
  4. Watch the log: ~/.local/share/opencode/log/opencode.log grows ~500+ lines/s, ~94% of them watcher subscribe / watcher started / watcher stopped for the skill roots.
  5. sample <pid> shows the time is entirely in the native watcher.

Expected: watchers subscribe once and then sit idle; CPU near zero.
Actual: continuous subscribe → started → stopped → subscribe churn; CPU stays pegged until the process is restarted.

Reproduces consistently on every fresh server launch, with no user interaction and no filesystem changes.

Expected Behavior

Skill-directory watchers should subscribe once, stay subscribed, and idle. No resubscription should occur absent filesystem activity.

Actual Behavior

The watcher loops. Samples show only @parcel/watcher frames (.bun-501-*.node):

FSEventsBackend::startStream → FSEventStreamStart (register_with_server / f2d_register_rpc)
FSEventsBackend::unsubscribe → FSEventStreamStop (dispose_f2d_private_port / f2d_unregister_rpc)
Watcher::triggerCallbacks → Watcher::isIgnored → Glob::isIgnored
Backend::watch / Backend::unwatch

Log growth during a 6-second window:

total lines:        2808
message="watcher subscribe":  high
message="watcher stopped":    ~257/4s
message="watcher started":    ~257/4s
non-watcher lines:   0

Watched paths, all repeated:

~/.claude/skills
~/.agents/skills
<project>/.claude/skills
<project>/.agents/skills

watcher subscribe entries always carry ignores=0 (the skill dirs have no ignore patterns).

Not caused by file changes: a 6-second snapshot of every file/dir mtime under those four roots was byte-identical (diff empty), while the churn continued uninterrupted. So this is a replay/self-trigger loop inside the watcher, not a reaction to writes.

Additional Context

Disabling the file watcher stops it (A/B, same machine/paths):

Run Server CPU log lines/4s
watcher enabled 187% 1701, 1787
OPENCODE_DISABLE_FILEWATCHER=1 (relaunch) 12–18% 45, 0, 0

Env confirmed on the child: OPENCODE_DISABLE_FILEWATCHER=1, OPENCODE_FILEWATCHER_DISABLE=1. In the bundle these gate fs.filewatcher:
fs: { filewatcher: !truthy(process.env.OPENCODE_FILEWATCHER_DISABLE ?? process.env.OPENCODE_DISABLE_FILEWATCHER), fff: ... } — and the @opencode/Watcher service takes an enabled option.

Not the FFF watcher: #32511 root-causes a similar high-CPU symptom to the bundled FFF indexer (@ff-labs/fff-bun, notify-rs). Ours is the Bun/parcel watcher (FSEventsBackend), and OPENCODE_DISABLE_FILEWATCHER=1 (not OPENCODE_DISABLE_FFF) removes it. Worth noting the server here is launched with cwd $HOME, i.e. exactly #32511's trigger, so both problems may coexist.

Related issues: #32511 (high CPU; also flags an fsevents watcher duplication + re-subscribe amplifier), #37111 (@parcel/watcher subscribe under multi-project attach — deadlock, Linux), #35813 (closed; recursive parcel watcher per non-home Location), #21452 (parcel watcher callback shared across subscriptions).

Workaround: OPENCODE_DISABLE_FILEWATCHER=1 opencode serve ... — confirmed to stop the churn (loses live file-tree/hot-reload only). Also OPENCODE_DISABLE_FFF=1 is worth trying since the server runs from $HOME.

Happy to attach full sample output, the raw log window, and lsof on request.

Activity

  1. opencode-agent commented on Oct 7, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the A/B numbers.

    We tried to reproduce this on 2.0.24 on Linux with the same layout: ~/.claude/skills and ~/.agents/skills, plus .claude/skills and .agents/skills in a project. We ran opencode serve from $HOME and opened both the project and $HOME. On Linux each skill directory was watched once and never re-subscribed, and CPU stayed near zero. Your samples are all inside the macOS FSEvents watcher, so this looks specific to macOS. It needs a maintainer with a Mac to check further.

    Two things would help:

    Related issues, ordered by likelihood:

    Until then, OPENCODE_DISABLE_FILEWATCHER=1 remains the workaround, as you found.

  2. bbruen3 commented on Oct 7, 2026

    @bbruen3
    Author

    Thanks — both answers:

    Plain opencode serve (no OpenChamber) from $HOME, macOS: no loop. v2.0.21 opencode serve with SSE clients on $HOME and a project stayed ~0.5% CPU, while the client-driven server on the same machine was at ~67% over the same window. So plain serve alone does not reproduce for me.

    Writes under ~/.config/opencode during churn: none. I monitored the config root, ~/.config/openchamber, and ~/.local/state/opencode for 30s during active churn: 0 changes. So #53393's write-trigger isn't active here — this looks like the zero-activity variant in #53416.

    Consolidating all skill roots to one real dir cut it ~4× (CPU 288% → ~50%, log lines 1701/4s → 408/4s) but did not stop it — still looping over 3 watched paths (one global canonical dir plus the project's .claude/skills and .agents/skills). The watcher is left enabled.

    Given the overlap, this may be the macOS instance of #53416.

  3. Gladess1102 commented on Oct 18, 2026

    @Gladess1102

    Confirming this still happens on 2.0.32 (desktop app, bundled opencode-cli 2.0.32), and adding a variant of the loop plus two side effects that I didn't see mentioned here.

    Different failure mode: subscribe fails instead of churning

    In my case the loop is driven by subscribe errors rather than successful resubscribes. Same root, different entry point:

    message="watcher subscribe" path=<project>/.agents/skills
    level=ERROR message="failed to subscribe" directory=<project>/.agents/skills
      cause="Error: Error starting FSEvents stream
        at ConfigSkillPlugin.watch
        at ConfigSkillPlugin.load
        at ConfigSkillPlugin.refresh
        at Effect.fn"
    message="watcher stopped" path=~/.agents/skills
    message="watcher subscribe" path=~/.agents/skills   <-- immediately again
    

    Two things worth noting:

    • The failing path is fine. While the loop was running I called fs.watch() on the exact same directory from a separate Node process and it succeeded (all four skill roots, including the one that was failing). So the original error is transient — it's the retry behaviour that makes it permanent.
    • A failure on one directory also tears down and recreates watchers for the other directories, so one bad path keeps resetting all of them.

    Impact

    Metric Idle During the loop
    opencode-cli CPU ~2% 158%
    opencode-cli RSS ~800 MB 1660 MB
    macOS compressor 1.5 GB 4.5 GB
    system CPU idle ~80% 0.8%

    failed to subscribe appeared 3796 times in one session (roughly 1–3/s at peak). The machine was unusable — not just high CPU, but memory pressure pushing everything else into swap.

    RSS did come back down (~816 MB) once the loop stopped on its own, so I don't think this is a permanent leak — but the balloon is large enough to take a 16 GB machine down while it lasts.

    Quitting the desktop app does not clear it

    opencode-cli serve --service survives app quit as an orphan (PPID 1) and keeps the elevated RSS. Relaunching the app reconnects to that same orphan, so restarting the app looks like it does nothing. Only pkill -f "opencode-cli serve --service" clears it. Might be worth handling separately — it makes this bug much harder to get out of.

    Trigger

    Same as yours: multiple project directories open, several skill roots. Mine appeared spontaneously after long uptime. OPENCODE_DISABLE_FILEWATCHER=1 sounds like the right workaround — thanks for finding it, I'll use that.

    Happy to provide the raw log window or a sample if useful. macOS 26.5.1 / M1 / 16 GB.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions