You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Plugins listed by absolute path in plugins are silently never loaded; plugin import failures produce no log #58647
Two silent plugin-loading failures we hit while debugging a compaction outage:
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.)
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.
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:
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.
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"<<EOFexport default { id: "$2", setup() { return {} } }EOF
}
run_case() { # name setup-fnlocal name=$1;shiftlocal 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$pid2>/dev/null; pkill -f "serve --port $port"2>/dev/null; sleep 1
echoecho"-- 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 loadcase_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 elsewherecase_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"
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.
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.
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.
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.
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.
Activity
opencode-agent commentedon Oct 24, 2026
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:
"plugins": ["<abs>/my-plugin"], withpackage.json("main": "index.js") and a minimal default-export plugin.index.jsis a regular file, the plugin loads (loading pluginis logged andGET /api/pluginshows it as active).index.jsis 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.tsfollows 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 asWARN failed to load plugin. Since you saw noloading pluginline, 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 thepluginsentry 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)
Run it:
bash repro.shmariaa144 commentedon Oct 24, 2026
Thanks — that matches what we see, and the pointer to
packages/core/src/config/plugin/source.tsexplains it.Adding deployment context in case it helps others: this reproduces
naturally with NixOS / home-manager-managed configs, because
home.file/xdg.configFileentries symlink into the nix store, soa 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 reachesit — 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
.jsfileauto-discovered next to it loaded fine.
Workaround we settled on for symlinked setups: ship the plugin as a
loose
.jsfile inside the auto-discovered plugins directory anddrop the absolute-path
pluginsentry. Loads reliably acrossrestarts.
Dante-dan commentedon Oct 24, 2026
I added the discovery warnings in this reference patch:
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 checkpassed (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.mariaa144 commentedon Oct 25, 2026
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.
Hamza-Abili commentedon Oct 27, 2026
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.Hamza-Abili commentedon Oct 27, 2026
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.
fix(core): log skipped plugin discovery
fix(core): log skipped plugin discovery
fix(core): log skipped plugin discovery