Skip to content

Plugins listed by absolute path in plugins are silently never loaded; plugin import failures produce no log #58647

Open

Description

@mariaa144
  • opencode 2.0.32, Linux (NixOS), server + CLI.

Two silent plugin-loading failures we hit while debugging a compaction outage:

  1. A plugin directory referenced by absolute path in the plugins config array is never loaded. No loading plugin log line, no error, server or CLI. The plugins guide lists "/absolute/path/plugin" as a supported form. (An npm package entry in the same array loads fine, and a loose .js file auto-discovered from the plugins directory loads fine — only the absolute-path directory entry is skipped.)

  2. A plugin whose module fails to import also fails silently. In our case the plugin imported @opencode/plugin but module resolution from its deployed location (a symlink into the Nix store) cannot find the package; nothing is logged anywhere.

Repro (case 1):

// ~/.config/opencode/opencode.jsonc
{ "plugins": ["/home/me/.config/opencode/plugins/my-plugin"] }

with my-plugin/package.json ("main": "index.js") and an index.js that default-exports a plugin registering any session hook. Restart the service, then:

grep "loading plugin" ~/.local/share/opencode/log/opencode.log

The plugin never appears and its hooks never fire. No error is logged at any level.

Impact: a full deploy looked completely healthy while the plugin did nothing — every session compaction on the machine was failing and the logs gave zero indication the fix hadn't loaded. The CLI's plugin reconciliation count didn't change when the plugin was added either, so there is no signal anywhere that loading was skipped.

Request: log a warning or error when a plugins entry is skipped or its module fails to import (entry + reason), and consider a startup line listing the plugin ids actually loaded per location.

Activity

  1. opencode-agent commented on Oct 24, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced the first problem on 2.0.32, and it still happens on the latest dev (b64cc80d).

    What I did:

    • Configured "plugins": ["<abs>/my-plugin"], with package.json ("main": "index.js") and a minimal default-export plugin.
    • When index.js is a regular file, the plugin loads (loading plugin is logged and GET /api/plugin shows it as active).
    • When index.js is a symlink to a file outside the plugin directory, as home-manager does with /nix/store, the plugin never loads. It is missing from /api/plugin, and nothing is logged at any level, even with --log-level debug.

    The likely cause, from reading the code: the scan step in packages/core/src/config/plugin/source.ts follows symlinks and then requires the entrypoint to be inside the plugin directory. If it isn't, the entry is dropped without a log line. A directory with no entrypoint is dropped the same way (#52300, #58574).

    I could not confirm the second point. Once a plugin reaches the loader, import errors (including an unresolvable @opencode/plugin, see #50434 / #49608) are logged as WARN failed to load plugin. Since you saw no loading plugin line, your plugin was most likely dropped at this earlier step, before any import was tried.

    Until this is fixed, a possible workaround: make index.js (and the other files) real copies inside the plugin directory rather than symlinks. Alternatively, point the plugins entry at the real directory in the store (I only tested this setup with files that are not symlinks).

    The script below reproduces the three cases (plain file, symlinked entrypoint, and a directory outside the config dir).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for anomalyco/opencode#58647: absolute-path plugin directory in `plugins`
    # is silently dropped when its entrypoint is a symlink to a file outside the
    # directory (as with NixOS/home-manager, which symlinks files into /nix/store).
    # Usage: OC="opencode" bash repro.sh   (or OC="bun ... packages/cli/src/index.ts")
    set -u
    OC=${OC:-opencode}
    export OPENCODE_PASSWORD=repro58647
    ROOT=$(mktemp -d /tmp/opencode/repro58647.XXXX)
    
    write_plugin() { # dir id
      mkdir -p "$1"
      echo "{\"name\":\"$2\",\"type\":\"module\",\"main\":\"index.js\"}" > "$1/package.json"
    }
    plugin_src() { # file id
      cat > "$1" <<EOF
    export default { id: "$2", setup() { return {} } }
    EOF
    }
    
    run_case() { # name setup-fn
      local name=$1; shift
      local H="$ROOT/$name/home"; local P="$ROOT/$name/project"
      mkdir -p "$H/.config/opencode/plugins" "$P"
      export HOME="$H" XDG_CONFIG_HOME="$H/.config" XDG_DATA_HOME="$H/.local/share" XDG_STATE_HOME="$H/.local/state" XDG_CACHE_HOME="$H/.cache"
      "$@" "$H"
      echo "===== case: $name ====="
      cat "$H/.config/opencode/opencode.jsonc"
      local port=$((4100 + RANDOM % 800))
      (cd "$P" && exec $OC serve --port $port --hostname 127.0.0.1 --print-logs --log-level debug >"$ROOT/$name/stderr.log" 2>&1) &
      local pid=$!
      sleep 6
      (cd "$P" && $OC api --server http://127.0.0.1:$port GET /api/plugin >/dev/null 2>&1)
      sleep 4
      echo; echo "-- /api/plugin after 10s:"
      (cd "$P" && $OC api --server http://127.0.0.1:$port GET /api/plugin 2>&1) | python3 -c 'import json,sys; d=json.load(sys.stdin)["data"]; print("non-builtin plugins:", [(p["id"], p["state"]["status"]) for p in d if p["source"]["type"]!="builtin"])'
      kill $pid 2>/dev/null; pkill -f "serve --port $port" 2>/dev/null; sleep 1
      echo
      echo "-- plugin-related log lines:"
      local lines; lines=$(grep -E "loading plugin|failed to load plugin|configured plugin|level=(WARN|ERROR)" "$ROOT/$name/stderr.log" | head -20)
      echo "${lines:-(none - nothing logged at any level, log-level=debug)}"
    }
    
    # A: plain directory with real index.js -> expected to load
    case_plain() {
      local d="$1/.config/opencode/plugins/my-plugin"; write_plugin "$d" plain-plugin; plugin_src "$d/index.js" plain-plugin
      echo "{ \"plugins\": [\"$d\"] }" > "$1/.config/opencode/opencode.jsonc"
    }
    # B: directory whose index.js is a symlink to a file elsewhere (like /nix/store)
    case_symlinked_file() {
      local d="$1/.config/opencode/plugins/my-plugin"; write_plugin "$d" nix-plugin
      mkdir -p "$1/store/abc-my-plugin"; plugin_src "$1/store/abc-my-plugin/index.js" nix-plugin
      ln -s "$1/store/abc-my-plugin/index.js" "$d/index.js"
      echo "{ \"plugins\": [\"$d\"] }" > "$1/.config/opencode/opencode.jsonc"
    }
    # C: absolute path outside the plugins dir, entrypoint symlinked elsewhere
    case_outside() {
      local d="$1/plugins-elsewhere/my-plugin"; write_plugin "$d" outside-plugin
      mkdir -p "$1/store/def-my-plugin"; plugin_src "$1/store/def-my-plugin/index.js" outside-plugin
      ln -s "$1/store/def-my-plugin/index.js" "$d/index.js"
      echo "{ \"plugins\": [\"$d\"] }" > "$1/.config/opencode/opencode.jsonc"
    }
    
    run_case plain case_plain
    run_case symlinked-file case_symlinked_file
    run_case outside case_outside
    echo "artifacts in $ROOT"

    Run it: bash repro.sh

  2. mariaa144 commented on Oct 24, 2026

    @mariaa144
    Author

    Thanks — that matches what we see, and the pointer to
    packages/core/src/config/plugin/source.ts explains it.

    Adding deployment context in case it helps others: this reproduces
    naturally with NixOS / home-manager-managed configs, because
    home.file / xdg.configFile entries symlink into the nix store, so
    a plugin directory's entrypoint resolves outside the plugin
    directory by construction. The same applies to any dotfiles manager
    that symlinks config trees.

    One more data point for the "silent" part: the loader does log
    import failures (WARN failed to load plugin) once a plugin reaches
    it — we confirmed that separately. Our first attempt never got that
    far, which is what made this confusing to debug: the config-listed
    plugin produced no log line at all, while a loose .js file
    auto-discovered next to it loaded fine.

    Workaround we settled on for symlinked setups: ship the plugin as a
    loose .js file inside the auto-discovered plugins directory and
    drop the absolute-path plugins entry. Loads reliably across
    restarts.

  3. Dante-dan commented on Oct 24, 2026

    @Dante-dan
    Contributor

    I added the discovery warnings in this reference patch:

    • A directory with no server entrypoint now logs its path and the reason it was skipped.
    • An entrypoint resolving outside the plugin directory now logs the configured path, resolved root, and resolved server path.

    This makes the home-manager case visible before the loader runs. The existing containment rule remains in place, and import failures keep their existing loader warning.

    Local validation: bun run check passed (34 tasks). The existing plugin suites had 15 passing tests and one native watcher test timing out after 5 seconds; that same test also times out on the unchanged base.

  4. mariaa144 commented on Oct 25, 2026

    @mariaa144
    Author

    Thanks — that patch covers both of the silent cases we hit.

    From our deployment side: with the containment warning logging the configured path, resolved root, and resolved server path, our original failure (home-manager symlinked plugin directory resolving into the nix store) would have produced a log line naming the plugin and the reason it was skipped — exactly the signal that was missing while every compaction on the machine was failing silently.

    The loose-file workaround keeps working for us, so there's no urgency on our end. Happy to run the reference branch on a NixOS box with a symlinked plugin tree and report what the new warnings look like in practice before it lands.

  5. Hamza-Abili commented on Oct 27, 2026

    @Hamza-Abili

    I would like to take this if @Dante-dan is not already opening a PR. The diagnosis points at packages/core/src/config/plugin/source.ts: I would add the two discovery warnings (no server/GUI/TUI entrypoint, and an entrypoint resolving outside the plugin directory), keep the containment behavior unchanged, and add focused tests. If @Dante-dan plans to open a PR with the reference patch, I will stand down and help review/test instead.

  6. Hamza-Abili commented on Oct 27, 2026

    @Hamza-Abili

    Opened #59707 with the discovery warnings and focused tests, crediting @Dante-dan for the reference patch. If you already have a PR in flight, tell me and I will stand down/close mine so we do not duplicate work.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions