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
cli: opencode plugin list prints "No plugins found" on the first call per cold location #54654
Summary:opencode plugin list returns an empty list when invoked before the working directory's plugin scan finishes, then lists everything on the second call. Same cold-location race as #54192 (mcp.list), #51680 (skill.list), #51282 (agent.list), and #51703 (model.list) — plugin.list appears to be the remaining handler that reads the registry without awaiting plugin activation (contrast model.list, which does yield* awaitPlugins; fixed for skills in #51687).
Evidence: log timeline from a live repro — empty response at 09:29:13 with loading plugin for that directory logged 70 ms into the same call; three repeat calls at 09:33 across three directories all list the plugin. The plugin itself loads and works in sessions the whole time; only the listing races.
Expected:plugin.list awaits plugin activation for the requested location (the #54191/#51687 pattern) before reading the registry.
Plugins
No response
OpenCode version
v2.0.26
Steps to reproduce
Repro (2.0.26, Linux x64):
Drop a local plugin file into ~/.config/opencode/plugins/ (e.g. codesearch.ts — server-side discovery accepts bare .ts files).
opencode service restart.
Immediately run opencode plugin list → "No plugins found" (exit 0).
Run it again → the plugin is listed. Log shows msg="loading plugin" id=…/codesearch.ts timestamped inside the first (empty) call — the list request itself triggers the load, then reads the registry before it completes.
Confirmed the premise by reading the current dev source: plugin.list in packages/server/src/handlers/plugin.ts calls Plugin.Service.use((plugin) => plugin.list()) without waiting
for activation, and Plugin.list returns the inventory binding that starts as [] and is only replaced
after a generation activates (packages/core/src/plugin.ts). So a cold Location's first call observes the
empty registry — the same shape fix(server): await models before listing (#41783) addressed for model.list, where the handler now does yield* awaitPlugins from packages/server/src/location.ts.
Plan: make plugin.list await activation before reading the registry (the model.list pattern, with the
same five-second ServiceUnavailableError guard), plus a regression test that asserts a cold-location list
call returns the plugin instead of an empty array. I'll verify against opencode serve from source.
Thanks for the detailed report. I reproduced this on 2.0.26, and it still happens on the latest dev (707de58) run from source.
Steps (the script below does this in an isolated config):
Put a local plugin file codesearch.ts (written with Plugin.define) in the global opencode/plugins/ config folder.
Restart the service (opencode service restart).
In each of three new directories, run opencode plugin list twice.
In every directory, the first call printed No plugins found and the next call listed codesearch. That fits your analysis: plugin.list in packages/server/src/handlers/plugin.ts reads the plugin list without first waiting for plugins to finish loading (awaitPlugins), which model.list does. Related to the same race in other handlers: #54192 / #54191 (MCP), #51680 (skills) and #51282 (agents).
Reproduction script (repro.sh)
#!/usr/bin/env bash# Repro for #54654: `opencode plugin list` prints "No plugins found" on the first# call in each directory the service has not loaded yet; the second call lists the plugin.# ./repro.sh -> uses `opencode` from PATH (e.g. @opencode/cli@2.0.26)# OC_DEV=/path/to/checkout ./repro.sh -> runs the CLI and service from sourceset -u
oc() {
if [ -n"${OC_DEV:-}" ];then
bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js""$OC_DEV/packages/cli/src/index.ts""$@"else
opencode "$@"fi
}
mkdir -p /tmp/opencode
export HOME=$(mktemp -d /tmp/opencode/home.XXXX)# isolated config/stateexport XDG_CONFIG_HOME=$HOME/.config XDG_DATA_HOME=$HOME/.local/share XDG_STATE_HOME=$HOME/.local/state XDG_CACHE_HOME=$HOME/.cache
mkdir -p "$XDG_CONFIG_HOME/opencode/plugins"
cat >"$XDG_CONFIG_HOME/opencode/plugins/codesearch.ts"<<'P'import { Plugin } from "@opencode/plugin"export default Plugin.define({ id: "codesearch", async setup() {} })Pecho"version: $(oc --version)"if [ -n"${OC_DEV:-}" ];then# from source, the auto-spawned service lacks the preload, so start it ourselves
(cd "$HOME"&& oc serve --service >"$HOME/serve.log"2>&1) & sleep 8
else
oc service restart >/dev/null 2>&1|| oc service start >/dev/null 2>&1fifordin a b c;do
mkdir -p "$HOME/proj-$d";cd"$HOME/proj-$d"echo"== proj-$d: first call"; oc plugin list 2>&1echo"== proj-$d: second call"; oc plugin list 2>&1doneif [ -n"${OC_DEV:-}" ];then pkill -f "index.ts serve --service";else oc service stop >/dev/null 2>&1;fi
Confirming on v2.0.32 (macOS 26). With a globally configured local plugin (~/.config/opencode/plugins/orchestrator.ts, promise API), opencode plugin list prints "No plugins found" when run from the home directory or a fresh project, and lists the plugin when run from ~/.config/opencode itself; the plugin is in fact loaded and its tools work in both cases (the service log shows "loading plugin"). Note the config dir here is a real directory whose entries (plugins, agents, ...) are symlinks into a git checkout, which works fine for loading, so this looks like a listing-scope issue rather than the symlink problem in #55119.
Confirming on 2.1.4, macOS arm64 — and the same race has a harder failure mode than the cosmetic No plugins found: opencode plugin update <name>errors with a misleading message and exit 1.
$ opencode service stop && sleep 1
$ opencode plugin update opencode-self-improvement
Error: Plugin is not configured: opencode-self-improvement # exit 1
at cli.plugin.inspect ...
$ sleep 3
$ opencode plugin update opencode-self-improvement
No plugin updates available
Timeline from the service log: the CLI spawns the cold service, location services booted at T+0.05s, msg="loading plugin" id=opencode-self-improvement at T+0.07s — while the CLI's plugin.list request had already been served from the empty inventory. cli.plugin.inspect (packages/cli/src/commands/handlers/plugin/inventory.ts) then fails with Plugin is not configured: <name> because it finds neither a server item nor a cli.json TUI item — so the user is told their correctly-configured global opencode.json plugin doesn't exist.
Typical trigger: opencode plugin add <pkg> (which never touches the server) immediately followed by opencode plugin update <pkg> — reported downstream as abogartz/opencode-self-improvement#115. The plugin itself loads and works throughout.
#56998 (adding awaitPlugins to plugin.list/check/update) covers this too, since inspect reads plugin.list.
Confirming on OpenCode 2.2.0, macOS, with "plugins": ["@dietrichgebert/ponytail"] in the global opencode.json.
opencode plugin check @dietrichgebert/ponytail --standalone exits with code 1: Plugin is not configured: @dietrichgebert/ponytail. In one reproduction, the same server logged loading plugin 1 ms after the CLI error.
Our updater exhausted 10 retries, each starting a fresh standalone server.
As a control, I started one private server with opencode serve --hostname 127.0.0.1 --port 0 and ran three checks against it using --server. The first two failed with the same error; the third succeeded and reported Ponytail 5.3.2 as current. No update was performed, and the private server was stopped afterward.
This appears consistent with the activation race discussed here. I have not tested #56998.
Also reproduces on v2.2.5 (Linux x64, WSL2). The background service had been running for more than a day and each new working directory was cold again, so the empty answer is not limited to the first call after a service start.
For plugin update this means a pending update is skipped without an error. plugin add writes the unversioned name to plugins, so this is not the exact-revision case that plugin update skips by design. Transcript from the script below (local registry, plugin add without a version, then 1.7.0 is published; $ROOT is the temp directory):
$ opencode plugin add @repro/opencode-plugin # registry latest is 1.6.1
Plugin "@repro/opencode-plugin" installed and added to $ROOT/config/opencode/opencode.json
$ cd proj1 && opencode plugin check # first call in this directory
No package plugins found
$ sleep 8 && opencode plugin check
Server
repro.plugin 1.6.1 (current)
# 1.7.0 is published
$ opencode plugin check # proj1, warm
Server
repro.plugin 1.6.1 (update available)
$ cd ../proj2 && opencode plugin update # new directory
No plugin updates available # exit 0
$ cd ../proj3 && opencode plugin update @repro/opencode-plugin # another new directory
Error: Plugin is not configured: @repro/opencode-plugin # exit 1
# the cache still has only the 1.6.1 generation
$ sleep 8 && opencode plugin update # proj3 again
Updated Server plugin "repro.plugin"
The package stays in the global opencode.json throughout. The cold plugin update exits 0 with No plugin updates available, so a script cannot tell it from "everything is current" and has nothing to retry on.
On a real setup with 4 server plugins (2 from npm, 2 local), two plugin check calls about a second apart in a new directory were both empty, and a call about 5 s after the first listed both npm plugins. #56998 was not tested.
Reproduction script (repro.sh)
#!/usr/bin/env bash# plugin check/update on a location whose plugins have not activated yet (#54654).# Needs node, npm, curl and opencode on PATH. Config, cache, state and the service are isolated under $ROOT.
OC=${OC:-opencode}
NAME=@repro/opencode-plugin
FILE=${NAME#*/}
PORT=${PORT:-47811}
ROOT=$(mktemp -d)
TO=$(command -v timeout ||command -v gtimeout)
mkdir -p "$ROOT"/{home,config/opencode,data,cache,state,tarballs,pkgsrc} "$ROOT"/proj{1,2,3}
# Mock registry with one package. 1.6.1 is visible first; POST /publish makes 1.7.0 visible.
cat >"$ROOT/registry.mjs"<<'EOF'import http from "node:http"import fs from "node:fs"import path from "node:path"import crypto from "node:crypto"const [root, port, name] = process.argv.slice(2)const file = name.split("/").pop()const releases = ["1.6.1", "1.7.0"]let published = 1const digest = (algo, buf, encoding) => crypto.createHash(algo).update(buf).digest(encoding)const tarball = (v) => fs.readFileSync(path.join(root, "tarballs", `${file}-${v}.tgz`))http .createServer((req, res) => { const url = decodeURIComponent(req.url.split("?")[0]) if (req.method === "POST" && url === "/publish") { published = releases.length return res.end("published") } if (url === `/${name}`) { const versions = {} for (const v of releases.slice(0, published)) { const buf = tarball(v) versions[v] = { name, version: v, type: "module", main: "./index.js", exports: { ".": { import: "./index.js", default: "./index.js" } }, dist: { tarball: `http://127.0.0.1:${port}/${name}/-/${file}-${v}.tgz`, shasum: digest("sha1", buf, "hex"), integrity: "sha512-" + digest("sha512", buf, "base64"), }, } } res.setHeader("content-type", "application/json") return res.end(JSON.stringify({ name, "dist-tags": { latest: releases[published - 1] }, versions })) } const m = url.match(/\/-\/.+-(\d+\.\d+\.\d+)\.tgz$/) if (m && releases.includes(m[1])) return res.end(tarball(m[1])) res.statusCode = 404 res.end() }) .listen(Number(port), "127.0.0.1")EOFforvin 1.6.1 1.7.0;do
d="$ROOT/pkgsrc/$v"; mkdir -p "$d"echo"{ \"name\": \"$NAME\", \"version\": \"$v\", \"type\": \"module\", \"main\": \"./index.js\", \"exports\": { \".\": { \"import\": \"./index.js\", \"default\": \"./index.js\" } } }">"$d/package.json"echo'export default { id: "repro.plugin", setup() {} }'>"$d/index.js"
tgz=$(cd "$d"&& HOME="$ROOT/home" npm pack --silent --pack-destination "$ROOT/tarballs")
mv "$ROOT/tarballs/$tgz""$ROOT/tarballs/$FILE-$v.tgz"done
node "$ROOT/registry.mjs""$ROOT""$PORT""$NAME"& REG=$!echo"registry=http://127.0.0.1:$PORT/">"$ROOT/npmrc"echo'{ "update": "disable" }'>"$ROOT/config/opencode/opencode.json"oc() {
env -i PATH="$PATH" HOME="$ROOT/home" XDG_CONFIG_HOME="$ROOT/config" XDG_DATA_HOME="$ROOT/data" \
XDG_CACHE_HOME="$ROOT/cache" XDG_STATE_HOME="$ROOT/state" NPM_CONFIG_USERCONFIG="$ROOT/npmrc" \
OPENCODE_DISABLE_AUTOUPDATE=1 NO_COLOR=1 no_proxy=127.0.0.1,localhost \
${TO:+$TO 120}$OC"$@"2>&1| grep -v -E '^[[:space:]]+at '| head -n 3
echo"[exit=${PIPESTATUS[0]}]"
}
installed() { grep -h -o '"version": *"[^"]*"'"$ROOT"/cache/opencode/npm/$NAME@latest/*/node_modules/$NAME/package.json | tr '\n''';echo; }
trap'(cd "$ROOT/proj1" && oc service stop >/dev/null); kill $REG' EXIT
sleep 1
# only needed when another service already listens on the default port
(cd "$ROOT/proj1"&& oc service set port 47812 >/dev/null)
cd"$ROOT/proj1"echo"## plugin add, registry latest is 1.6.1"
oc plugin add $NAME
grep -A2 '"plugins"'"$ROOT/config/opencode/opencode.json"
installed
echo"## proj1: first check is cold, the next one after 8s is warm"
oc plugin check; sleep 8; oc plugin check
echo"## registry publishes 1.7.0"
curl -s -X POST http://127.0.0.1:$PORT/publish;echoecho"## proj1 (warm): check"
oc plugin check
echo"## proj2 (new directory): update"cd"$ROOT/proj2"; oc plugin update
echo"## proj3 (another new directory): update with a target"cd"$ROOT/proj3"; oc plugin update $NAMEecho"## installed versions after the two updates above:"; installed
echo"## proj3 again after 8s: update"
sleep 8; oc plugin update
echo"## installed versions:"; installed
echo"## final check"
oc plugin check
echo"state kept in $ROOT"
We keep encountering this across servers: opencode plugin list sometimes reports no plugins on the first run, and opencode plugin update can consequently report no updates even when an update is available.
Waiting for activation, as proposed in #56998, may address the immediate race, but could the longer-term design separate configured/installed plugins from currently running plugins? Listing configured plugins should not require them to start successfully; runtime status could be shown separately.
Similarly, checking for or installing updates should identify packages from configuration and installed metadata, rather than depend on the active plugin inventory. Otherwise a slow or failed activation can make an installed plugin appear absent.
When an update replaces a running plugin, graceful shutdown is a separate concern: active instances may need to finish or safely stop work and release resources before replacement. The plugin API already supports an unload cleanup function—does the update path wait for that cleanup across affected instances and report failures or uncertain outcomes?
Activity
argszero commentedon Oct 12, 2026
I'd like to work on this.
Confirmed the premise by reading the current
devsource:plugin.listinpackages/server/src/handlers/plugin.tscallsPlugin.Service.use((plugin) => plugin.list())without waitingfor activation, and
Plugin.listreturns theinventorybinding that starts as[]and is only replacedafter a generation activates (
packages/core/src/plugin.ts). So a cold Location's first call observes theempty registry — the same shape
fix(server): await models before listing(#41783) addressed formodel.list, where the handler now doesyield* awaitPluginsfrompackages/server/src/location.ts.Plan: make
plugin.listawait activation before reading the registry (themodel.listpattern, with thesame five-second
ServiceUnavailableErrorguard), plus a regression test that asserts a cold-location listcall returns the plugin instead of an empty array. I'll verify against
opencode servefrom source.opencode-agent commentedon Oct 12, 2026
Thanks for the detailed report. I reproduced this on 2.0.26, and it still happens on the latest
dev(707de58) run from source.Steps (the script below does this in an isolated config):
codesearch.ts(written withPlugin.define) in the globalopencode/plugins/config folder.opencode service restart).opencode plugin listtwice.In every directory, the first call printed
No plugins foundand the next call listedcodesearch. That fits your analysis:plugin.listinpackages/server/src/handlers/plugin.tsreads the plugin list without first waiting for plugins to finish loading (awaitPlugins), whichmodel.listdoes. Related to the same race in other handlers: #54192 / #54191 (MCP), #51680 (skills) and #51282 (agents).Reproduction script (repro.sh)
Run it:
bash repro.shyouthtrouble commentedon Oct 18, 2026
Confirming on v2.0.32 (macOS 26). With a globally configured local plugin (
~/.config/opencode/plugins/orchestrator.ts, promise API),opencode plugin listprints "No plugins found" when run from the home directory or a fresh project, and lists the plugin when run from~/.config/opencodeitself; the plugin is in fact loaded and its tools work in both cases (the service log shows "loading plugin"). Note the config dir here is a real directory whose entries (plugins,agents, ...) are symlinks into a git checkout, which works fine for loading, so this looks like a listing-scope issue rather than the symlink problem in #55119.ZYMCao commentedon Oct 26, 2026
Confirmed as well on my opencode
2.1.4at macOS 15.6 (Darwin 24.6.0, arm64)abogartz commentedon Oct 26, 2026
Confirming on 2.1.4, macOS arm64 — and the same race has a harder failure mode than the cosmetic
No plugins found:opencode plugin update <name>errors with a misleading message and exit 1.Timeline from the service log: the CLI spawns the cold service,
location services bootedat T+0.05s,msg="loading plugin" id=opencode-self-improvementat T+0.07s — while the CLI'splugin.listrequest had already been served from the empty inventory.cli.plugin.inspect(packages/cli/src/commands/handlers/plugin/inventory.ts) then fails withPlugin is not configured: <name>because it finds neither a server item nor acli.jsonTUI item — so the user is told their correctly-configured globalopencode.jsonplugin doesn't exist.Typical trigger:
opencode plugin add <pkg>(which never touches the server) immediately followed byopencode plugin update <pkg>— reported downstream as abogartz/opencode-self-improvement#115. The plugin itself loads and works throughout.#56998 (adding
awaitPluginstoplugin.list/check/update) covers this too, sinceinspectreadsplugin.list.szafranski commentedon Oct 27, 2026
Confirming on OpenCode 2.2.0, macOS, with
"plugins": ["@dietrichgebert/ponytail"]in the globalopencode.json.opencode plugin check @dietrichgebert/ponytail --standaloneexits with code 1:Plugin is not configured: @dietrichgebert/ponytail. In one reproduction, the same server loggedloading plugin1 ms after the CLI error.Our updater exhausted 10 retries, each starting a fresh standalone server.
As a control, I started one private server with
opencode serve --hostname 127.0.0.1 --port 0and ran three checks against it using--server. The first two failed with the same error; the third succeeded and reported Ponytail 5.3.2 as current. No update was performed, and the private server was stopped afterward.This appears consistent with the activation race discussed here. I have not tested #56998.
docs: troubleshooting row for opencode cold-service 'Plugin is not co…
linghengqian commentedon Nov 4, 2026
Also reproduces on v2.2.5 (Linux x64, WSL2). The background service had been running for more than a day and each new working directory was cold again, so the empty answer is not limited to the first call after a service start.
For
plugin updatethis means a pending update is skipped without an error.plugin addwrites the unversioned name toplugins, so this is not the exact-revision case thatplugin updateskips by design. Transcript from the script below (local registry,plugin addwithout a version, then 1.7.0 is published;$ROOTis the temp directory):The package stays in the global
opencode.jsonthroughout. The coldplugin updateexits 0 withNo plugin updates available, so a script cannot tell it from "everything is current" and has nothing to retry on.On a real setup with 4 server plugins (2 from npm, 2 local), two
plugin checkcalls about a second apart in a new directory were both empty, and a call about 5 s after the first listed both npm plugins. #56998 was not tested.Reproduction script (repro.sh)
dluc commentedon Nov 7, 2026
We keep encountering this across servers:
opencode plugin listsometimes reports no plugins on the first run, andopencode plugin updatecan consequently report no updates even when an update is available.Waiting for activation, as proposed in #56998, may address the immediate race, but could the longer-term design separate configured/installed plugins from currently running plugins? Listing configured plugins should not require them to start successfully; runtime status could be shown separately.
Similarly, checking for or installing updates should identify packages from configuration and installed metadata, rather than depend on the active plugin inventory. Otherwise a slow or failed activation can make an installed plugin appear absent.
When an update replaces a running plugin, graceful shutdown is a separate concern: active instances may need to finish or safely stop work and release resources before replacement. The plugin API already supports an unload cleanup function—does the update path wait for that cleanup across affected instances and report failures or uncertain outcomes?