Skip to content

NixOS: native recursive FS watcher never loads (libstdc++.so.6 not found) #53210

Open

Description

@bew

Description

On the Nix build, opencode silently drops its native filesystem watcher and falls back to degraded watching.
The bundled @parcel/watcher addon is a prebuilt .node that links libstdc++.so.6 dynamically, with no RUNPATH.
On NixOS the loader can't resolve that library, so the dlopen fails and the watcher binding is not registered.

I see this both in the packaged output (nix build .#opencode) and in the flake.nix devshell.
Neither puts the C++ runtime on LD_LIBRARY_PATH, so the addon can never find it.

In practice the symptom is behavioral: edit a skill (or a config) and the running session never picks it up.
Recursive directory watches go silent, while the per-file watches still work.

I think this is the same defect as #18447 (which already proposed adding stdenv.cc.cc.lib to the wrapper) and #28964 (same failed to load watcher binding line).
Both were closed by the stale bot and it still reproduces, so I'm re-reporting with a narrower repro and a fix.

Plugins

(none)

OpenCode version

2.0.22 (d1f8f5b)

Steps to reproduce

The failure is swallowed at load time, so the behavioral repro is the clearest one:

  1. On NixOS, start opencode — either bun dev from the devshell, or the packaged binary from nix build .#opencode.
  2. Edit a skill under a watched skills directory, e.g. ~/.config/opencode/skills/<name>/SKILL.md.
  3. The skill does not reload.
    The recursive watch on the skills directory silently delivers no events, so the plugin never rescans.

To surface the underlying failure, add a temporary log to packages/core/src/filesystem/watcher.ts (revert afterwards):

const watcher = lazy((): typeof ParcelWatcher | undefined => {
  try {
    return createWrapper(loadBinding()) as typeof ParcelWatcher
  } catch (error) {
    console.error("[watcher] native binding failed to load", error)
    return
  }
})

It prints libstdc++.so.6: cannot open shared object file.
The server log shows the same thing from the other side — the recursive watch gives up:

message="watcher subscribe" path=~/.config/opencode/skills type=directory ignores=0
level=ERROR message="watcher backend not supported" directory=~/.config/opencode/skills platform=linux

Note the per-file watches (type=file / type=entries) still start with backend=node, so only the recursive directory watches silently break.
The devshell has the same gap, so bun dev fails there too.

Screenshot and/or share link

(none)

Operating System

NixOS

Terminal

WezTerm

Activity

  1. github-actions commented on Oct 4, 2026

    @github-actions
    Contributor

    This issue appears to overlap with existing reports. Please check:

    • #18447: Nix package missing the libstdc++.so.6 runtime dependency.
    • #28964: NixOS report with the same failed to load watcher binding / libstdc++.so.6 error.
  2. luochen1990 commented on Oct 27, 2026

    @luochen1990
    Contributor

    Adding production evidence from a long-running NixOS deployment (2.0.26, 0d61729b3f, Nix-packaged, opencode serve --service):

    • The daemon log shows watcher backend not supported 1333 times over a ~21-hour window (~1/min — it retries on every watch attempt). Verbatim except elided run/span ids and the project path:

      timestamp=2026-10-26T23:31:37.637Z level=ERROR run=... message="watcher backend not supported" directory=/home/lc/ws/<project>/.opencode platform=linux http.span=... role=server
      
    • Behavioral impact beyond skills: directory-level changes — new/removed/swapped plugin entries, and config files (which, as observed, aren't covered by a working per-file watch either) — never hot-reload. Per-file watches on already-loaded plugin entries still fire (that's how the rescan in the issue we filed separately (Plugins are silently unloaded after a plugin-dir symlink swap is followed by any rescan — NixOS home-manager deployments #59725) gets triggered). The only thing that eventually picks up directory-level changes is the supervisor's periodic full rescan (~24h), so a long-running service silently runs with stale config/plugins for up to a day.

    • One timing note connecting to that separate issue (Plugins are silently unloaded after a plugin-dir symlink swap is followed by any rescan — NixOS home-manager deployments #59725): because the directory watcher is dead, a plugin-dir symlink swap is not picked up immediately — the unload we describe there is delayed until the ~24h fallback rescan. The dead watcher changes the timing and visibility of that bug (delayed, mysterious, logs silent); the unload itself is its own defect.

    We now run a systemd path unit + idle gate that restarts the service on plugin-dir changes as a workaround, but a proper fix at the packaging layer (e.g. the stdenv.cc.cc.lib wrapper approach proposed in #18447 (linked upthread), or a static/bundled addon) would remove the whole class.

  3. opencodejr commented on Oct 27, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the fix PR. I could reproduce this, but not on NixOS itself: I hid the system libstdc++.so.6 on a regular Linux x64 machine to imitate it.

    What I found on 2.0.22:

    • The bundled @parcel/watcher addon needs libstdc++.so.6 and has no RUNPATH. The opencode binary itself doesn't need it.
    • With libstdc++ available, the ~/.config/opencode directory watch starts with backend=inotify.
    • With it hidden, the same watch logs watcher backend not supported ... platform=linux. The type=entries watches still start with backend=node.
    • With libstdc++ hidden but its directory on LD_LIBRARY_PATH (the approach in fix(nix): expose libstdc++ so the native fs watcher loads on NixOS #53213), the directory watch starts normally again.

    It still happens on the latest dev (fd8268e) run from source: with libstdc++ hidden, both ~/.config/opencode and ~/.config/opencode/skills log watcher backend not supported. nix/opencode.nix there still only puts wayland on LD_LIBRARY_PATH, and the devshell adds no libstdc++.

    I only checked the error in the logs, not that an edited skill fails to reload. The script below runs these checks; it needs root because it temporarily moves the system libstdc++ and puts it back on exit.

    Related: #18447 and #28964 (same missing libstdc++, closed as stale), #56255 (config edits not picked up on the Nix build, closed as a duplicate of this one) and #53213 (your fix).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #53210: the bundled @parcel/watcher native addon needs libstdc++.so.6
    # (dynamic NEEDED, no RUNPATH). When the loader can't resolve it — as on NixOS,
    # which has no global /lib with libstdc++ and whose Nix wrapper only adds wayland
    # to LD_LIBRARY_PATH — recursive directory watches fail with
    # "watcher backend not supported" and skill/config edits are never picked up.
    #
    # NixOS is simulated by hiding the system libstdc++ (needs root; restored on exit).
    # Usage: repro.sh [opencode command...]   (default: opencode)
    set -u
    OC=("${@:-opencode}")
    LIBDIR=/lib/x86_64-linux-gnu
    HIDE=/tmp/opencode/hidden-libstdc
    run() {
      local label=$1; shift
      local H; H=$(mktemp -d /tmp/opencode/home.XXXX)
      mkdir -p "$H/.config/opencode/skills/demo" "$H/proj"
      printf -- '---\nname: demo\ndescription: demo skill\n---\nv1\n' > "$H/.config/opencode/skills/demo/SKILL.md"
      (cd "$H/proj" && env HOME=$H XDG_CONFIG_HOME=$H/.config XDG_DATA_HOME=$H/.local/share \
        XDG_STATE_HOME=$H/.local/state XDG_CACHE_HOME=$H/.cache "$@" \
        timeout 30 "${OC[@]}" api --standalone --print-logs --log-level debug get /api/skill > "$H/log" 2>&1)
      echo "===== $label ====="
      grep -iE "type=directory|not supported|libstdc|binding" "$H/log" | grep -v stopped | sed 's/^.*message=/  message=/' | head -20
    }
    run "libstdc++ available (normal distro)"
    mkdir -p "$HIDE"
    mv $LIBDIR/libstdc++.so.6* "$HIDE"/ && ldconfig
    trap 'mv "$HIDE"/libstdc++.so.6* $LIBDIR/ && ldconfig' EXIT
    run "libstdc++ NOT resolvable (as on NixOS)"
    run "libstdc++ hidden, but its dir on LD_LIBRARY_PATH (proposed Nix wrapper fix)" LD_LIBRARY_PATH=$HIDE

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions