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: local plugin directory ignores package.json main and is silently skipped without a root entrypoint #52300
When a plugins config entry points to a local plugin directory, OpenCode resolves
the entrypoint at the package root (e.g. index.ts) and ignores the package's package.json"main" field. If no root-level entrypoint exists, the plugin is
silently skipped — no load attempt and no warning in the log.
Environment
opencode version: 2.0.20
OS: Windows 11 (Windows_NT 10.0.26200, win32 x64)
Terminal: VS Code terminal (TERM_PROGRAM=vscode)
Shell: Git Bash
Install/channel: latest
Active plugins: @tarquinen/opencode-dcp, @wakatime/opencode-wakatime, C:/repos/opencode-handoff, C:/repos/opencode-md-table-formatter
Reproduction
Have a local plugin directory whose package.json is "main": "src/plugin.ts" (no root-level index.ts).
The plugin is never loaded and nothing is logged about it.
Add a root index.ts re-exporting the plugin (export { default } from "./src/plugin") — the plugin then loads.
Related: pointing the config entry directly at the entrypoint file
(".../src/plugin.ts") is rejected with a warning: WARN message="configured plugin path must be a directory" target=C:/repos/opencode-handoff/src/plugin.ts
even though the V2 plugin migration guide documents local file paths
({"package": "./plugin/local.ts", ...}) as supported.
Expected Behavior
The directory loader should honor package.json"main" (or at least log a clear
warning when no resolvable entrypoint is found) instead of silently dropping the
plugin. File-path entries should load per the plugin migration guide.
Actual Behavior
Plugin is silently not loaded; no error, no warning. Only symptom is the plugin
missing from the msg="loading plugin" log lines.
Additional Context
Workaround: add a root-level index.ts re-exporting the default export from the
real entrypoint. Also note the "must be a directory" restriction conflicts with
the documented ["./plugin/local.ts", {...}] local-plugin form in the V1→V2
migration guide.
This issue might be a duplicate of existing issues. Please check:
#46551: Reports local plugin file paths being rejected and directories without an index entrypoint being dropped, closely matching both behaviors here.
#41530: Reports local directory packages not being discovered through their package.json entrypoints, similar to the main resolution issue here.
Same on 2.0.24 (macOS). It ignores exports too, not only main: a local directory with "exports": { ".": "./x.mjs" } and no index.* is skipped without any log line.
The same package installed with opencode plugin add git+file:///path/to/repo loads fine, because package installs do resolve exports. So a plugin can work when installed and still be invisible when you point plugins at its checkout to test it.
Hit the same bug class on 2.1.2 (Linux, release channel), but with a broader trigger condition: our directory does have a root-level entrypoint and a valid exports map, and it's still intermittently and silently skipped.
Environment
opencode version: 2.1.2
OS: Linux 6.12.88+deb13-amd64 (linux x64)
Shell: /bin/bash
Install/channel: latest (release)
Active plugins: local directory plugin (./plugins/caveman, relative to ~/.config/opencode/opencode.json), superpowers@git+https://github.com/obra/superpowers.git
Reproduction
Local plugin directory ~/.config/opencode/plugins/caveman/ with:
opencode service restart, then opencode api get /api/plugin.
Expected Behavior
Plugin appears in GET /api/plugin with state.status: "active", same as the sibling superpowers package entry, which loads reliably on every restart from the same config.
Actual Behavior
No msg="loading plugin" log line is ever emitted for this entry — not even an attempt, let alone a warning or state.status: "failed".
The entry is entirely absent from GET /api/plugin (only superpowers shows up).
Behavior is non-deterministic across otherwise-identical restarts with unmodified content: across ~10 clean opencode service restart cycles with the exact same files, it loaded successfully a small number of times and silently failed most of the time. No config, content, or path change correlated with success vs. failure in our testing.
We isolated variables extensively trying to find a deterministic trigger and could not:
Copying the identicalplugin.js/package.json content to a brand-new directory under /tmp or directly under $HOME (no dot-prefixed ancestor) sometimes loaded and sometimes didn't, with no code or config difference between attempts.
Pointing the entry at the plugin's own git-tracked path inside ~/.config/opencode/plugins/ behaved the same as pointing it at copies elsewhere — ruling out any special-casing of that specific directory.
A trivial throwaway plugin (export default Plugin.define({ id: "x", setup() { console.log("loaded") } })) at a fresh path did load successfully on at least one occasion via hot-reload after an opencode.json edit, without a full service restart — suggesting the reconciliation/hot-reload path for local directory plugins is itself unreliable, separate from any package.json resolution issue.
By contrast, the superpowers package-form entry in the very same config loaded successfully on 100% of the restarts we tried (confirmed via matching msg="loading plugin" id=superpowers@... entrypoint=file://.../superpowers.js).
Additional Context
This matches the symptom @vvo described in this thread (directory + exports, no log line at all) but in our case a root-level index-equivalent file is present and matches package.json"main"/"exports" exactly, so the "missing root entrypoint" explanation in the issue summary doesn't account for our failures.
No workaround found other than retrying opencode service restart repeatedly until GET /api/plugin happens to show the plugin as active, which is not viable for any workflow that depends on the plugin's hooks firing reliably (in our case, a plugin that tracks mode state via ctx.session.hook("prompt", ...) / ctx.session.hook("context", ...)).
Given the apparent non-determinism independent of path, content validity, and presence/absence of a root entrypoint, this may point to a race in plugin discovery/reconciliation rather than purely an exports resolution gap — happy to provide OPENCODE_LOG_LEVEL=DEBUG logs from a failing run if useful.
Still reproduces on 2.1.4 (macOS, Darwin 25.5.0 darwin arm64, release channel). Adding a data point that narrows the trigger, because this is easy to misdiagnose as a path-form problem — I did exactly that for an hour before reading the resolver.
The path form is not a factor
One config, five plugin directories, each with a distinct id. Four have a root index.ts; the fifth declares its entry only through package.jsonexports:
{
"plugins": [
"./rel",
"/abs/path/absolute",
"file:///abs/path/urlplug",
"./symlinked", // symlink to a directory outside the project"./exportsonly"// no root index; "exports": { ".": "./src/index.ts" }
]
}
Relative paths, absolute paths, file:// URLs and symlinked directories all load. Only exportsonly is missing, and it is completely silent: with --print-logs --log-level all, grep -ci 'exportsonly|exportsplug' returns 0, while the eight unrelated plugin entries in the same run each emit their msg="loading plugin" line.
Where it happens
Line references are from a source checkout at 0413ce2a28; the behaviour above was verified against the released 2.1.4 binary.
packages/plugin/src/host.ts:20-24 builds the specifier two different ways:
constspecifier=target.name
? [target.name,subpath].filter(Boolean).join("/")// installed package: node resolution, exports honored
: path.resolve(target.directory,subpath||"index")// local directory: a path, package.json never read
A configured directory has no name, so resolution is <dir>/server and then <dir>/index as filesystem paths. The directory's own package.json is never opened, which is why main and exports are both ignored — as @vvo describes above — and why the same package loads when installed and vanishes when you point plugins at its checkout.
The silence comes from packages/core/src/config/plugin/source.ts:167:
if(!entrypoints.server)return[]
The entry is dropped from the operation list, with no log at any level. #46551's Additional Context mentions a configured plugin directory has no index entrypoint warning for this branch; that string does not exist anywhere in the tree at 0413ce2a28, so the branch appears to have lost its diagnostic since then.
On the intermittent case
@TateWalker's non-determinism looks like a different problem. Here the behaviour was deterministic in both directions: with the entry only in exports, it failed every time; after adding a root index.ts that re-exports src/index.ts, it loaded on every run — roughly fifteen runs over an hour, mixing opencode run --standalone and a private opencode serve, each in a fresh temporary project. That said, none of my runs went through opencode service restart on the shared service, so this does not rule out a race in the path you are hitting.
Suggestion
The fix for the resolution gap is the one already in the title of this issue. Independently of it, a single warning at source.ts:167 naming the target and the subpaths that were tried (<dir>/server, <dir>/index) would turn this class of bug from an hour of bisecting into one line of log output.
Some context on main: in v2, a local plugin directory is resolved through its package.jsonexports, which only applies when the package.json also has a name. Otherwise OpenCode looks for the usual files at the root (server/index); main isn't read. So instead of a root index.ts, you can also add "name" and "exports": { ".": "./src/plugin.ts" } to your package.json. The current plugin migration guide also says configured local paths must be directories, and that a configured file is skipped with a warning.
The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
After upgrading to 2.2.0, the exact same config/directory that failed non-deterministically on every prior version (2.1.2, 2.1.3) has now loaded cleanly on 5 consecutive opencode service restart cycles, with zero failures — a complete reversal from the earlier ~10-restart test where it mostly failed.
No changelog entry we could find mentions this specifically, so it's possible it was fixed incidentally alongside other plugin-reconciliation work between 2.1.3 and 2.2.0. Flagging in case it's useful signal for whoever eventually chases this down, but as far as our case goes this is resolved.
Closing this as a duplicate of #58647, since there's been no reply in the last day saying how this is different. That issue covers local plugin directories being skipped with nothing in the log. The rejection of file-path entries (configured plugin path must be a directory) is tracked in #46551.
Until then, you can keep the root index.ts re-export, or add "name" and "exports": { ".": "./src/plugin.ts" } to the plugin's package.json instead of relying on main. If your problem is different from those issues, reply here and we can reopen.
Activity
github-actions commentedon Sep 30, 2026
This issue might be a duplicate of existing issues. Please check:
indexentrypoint being dropped, closely matching both behaviors here.package.jsonentrypoints, similar to themainresolution issue here.vvo commentedon Oct 7, 2026
Same on 2.0.24 (macOS). It ignores
exportstoo, not onlymain: a local directory with"exports": { ".": "./x.mjs" }and noindex.*is skipped without any log line.The same package installed with
opencode plugin add git+file:///path/to/repoloads fine, because package installs do resolveexports. So a plugin can work when installed and still be invisible when you pointpluginsat its checkout to test it.TateWalker commentedon Oct 21, 2026
Hit the same bug class on 2.1.2 (Linux, release channel), but with a broader trigger condition: our directory does have a root-level entrypoint and a valid
exportsmap, and it's still intermittently and silently skipped.Environment
./plugins/caveman, relative to~/.config/opencode/opencode.json),superpowers@git+https://github.com/obra/superpowers.gitReproduction
~/.config/opencode/plugins/caveman/with:plugin.js— valid V2 module:export default Plugin.define({ id: "caveman", setup(ctx) { ... } })package.jsonwith"type": "module", "main": "plugin.js", "exports": { ".": "./plugin.js" }"plugins": ["./plugins/caveman", "superpowers@git+https://github.com/obra/superpowers.git"]opencode service restart, thenopencode api get /api/plugin.Expected Behavior
Plugin appears in
GET /api/pluginwithstate.status: "active", same as the siblingsuperpowerspackage entry, which loads reliably on every restart from the same config.Actual Behavior
msg="loading plugin"log line is ever emitted for this entry — not even an attempt, let alone a warning orstate.status: "failed".GET /api/plugin(onlysuperpowersshows up).opencode service restartcycles with the exact same files, it loaded successfully a small number of times and silently failed most of the time. No config, content, or path change correlated with success vs. failure in our testing.plugin.js/package.jsoncontent to a brand-new directory under/tmpor directly under$HOME(no dot-prefixed ancestor) sometimes loaded and sometimes didn't, with no code or config difference between attempts.~/.config/opencode/plugins/behaved the same as pointing it at copies elsewhere — ruling out any special-casing of that specific directory.export default Plugin.define({ id: "x", setup() { console.log("loaded") } })) at a fresh path did load successfully on at least one occasion via hot-reload after anopencode.jsonedit, without a full service restart — suggesting the reconciliation/hot-reload path for local directory plugins is itself unreliable, separate from any package.json resolution issue.superpowerspackage-form entry in the very same config loaded successfully on 100% of the restarts we tried (confirmed via matchingmsg="loading plugin" id=superpowers@... entrypoint=file://.../superpowers.js).Additional Context
exports, no log line at all) but in our case a root-levelindex-equivalent file is present and matchespackage.json"main"/"exports"exactly, so the "missing root entrypoint" explanation in the issue summary doesn't account for our failures.opencode service restartrepeatedly untilGET /api/pluginhappens to show the plugin as active, which is not viable for any workflow that depends on the plugin's hooks firing reliably (in our case, a plugin that tracks mode state viactx.session.hook("prompt", ...)/ctx.session.hook("context", ...)).exportsresolution gap — happy to provideOPENCODE_LOG_LEVEL=DEBUGlogs from a failing run if useful.heliowap commentedon Oct 25, 2026
Still reproduces on 2.1.4 (macOS, Darwin 25.5.0 darwin arm64, release channel). Adding a data point that narrows the trigger, because this is easy to misdiagnose as a path-form problem — I did exactly that for an hour before reading the resolver.
The path form is not a factor
One config, five plugin directories, each with a distinct id. Four have a root
index.ts; the fifth declares its entry only throughpackage.jsonexports:{ "plugins": [ "./rel", "/abs/path/absolute", "file:///abs/path/urlplug", "./symlinked", // symlink to a directory outside the project "./exportsonly" // no root index; "exports": { ".": "./src/index.ts" } ] }Each plugin's
setuplogs its own id. Result:Relative paths, absolute paths,
file://URLs and symlinked directories all load. Onlyexportsonlyis missing, and it is completely silent: with--print-logs --log-level all,grep -ci 'exportsonly|exportsplug'returns0, while the eight unrelated plugin entries in the same run each emit theirmsg="loading plugin"line.Where it happens
Line references are from a source checkout at
0413ce2a28; the behaviour above was verified against the released 2.1.4 binary.packages/plugin/src/host.ts:20-24builds the specifier two different ways:A configured directory has no
name, so resolution is<dir>/serverand then<dir>/indexas filesystem paths. The directory's ownpackage.jsonis never opened, which is whymainandexportsare both ignored — as @vvo describes above — and why the same package loads when installed and vanishes when you pointpluginsat its checkout.The silence comes from
packages/core/src/config/plugin/source.ts:167:The entry is dropped from the operation list, with no log at any level. #46551's Additional Context mentions a
configured plugin directory has no index entrypointwarning for this branch; that string does not exist anywhere in the tree at0413ce2a28, so the branch appears to have lost its diagnostic since then.On the intermittent case
@TateWalker's non-determinism looks like a different problem. Here the behaviour was deterministic in both directions: with the entry only in
exports, it failed every time; after adding a rootindex.tsthat re-exportssrc/index.ts, it loaded on every run — roughly fifteen runs over an hour, mixingopencode run --standaloneand a privateopencode serve, each in a fresh temporary project. That said, none of my runs went throughopencode service restarton the shared service, so this does not rule out a race in the path you are hitting.Suggestion
The fix for the resolution gap is the one already in the title of this issue. Independently of it, a single warning at
source.ts:167naming the target and the subpaths that were tried (<dir>/server,<dir>/index) would turn this class of bug from an hour of bisecting into one line of log output.opencodejr commentedon Oct 28, 2026
Thanks for the detailed report and the workaround! I think this is a duplicate of #58647.
Related issues, ordered from most to least likely to be the same problem:
pluginsare silently never loaded; plugin import failures produce no log #58647: a local plugin directory set inpluginsby absolute path (with apackage.jsonmain) is never loaded, with nothing in the log. It asks for a warning whenever an entry is skipped, which is the silent part of your report..../src/plugin.tsare dropped withconfigured plugin path must be a directory. That's the second problem you describe.Some context on
main: in v2, a local plugin directory is resolved through itspackage.jsonexports, which only applies when thepackage.jsonalso has aname. Otherwise OpenCode looks for the usual files at the root (server/index);mainisn't read. So instead of a rootindex.ts, you can also add"name"and"exports": { ".": "./src/plugin.ts" }to yourpackage.json. The current plugin migration guide also says configured local paths must be directories, and that a configured file is skipped with a warning.The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
TateWalker commentedon Oct 28, 2026
Update: fixed for us as of 2.2.0.
After upgrading to 2.2.0, the exact same config/directory that failed non-deterministically on every prior version (2.1.2, 2.1.3) has now loaded cleanly on 5 consecutive
opencode service restartcycles, with zero failures — a complete reversal from the earlier ~10-restart test where it mostly failed.No changelog entry we could find mentions this specifically, so it's possible it was fixed incidentally alongside other plugin-reconciliation work between 2.1.3 and 2.2.0. Flagging in case it's useful signal for whoever eventually chases this down, but as far as our case goes this is resolved.
opencodejr commentedon Oct 29, 2026
Closing this as a duplicate of #58647, since there's been no reply in the last day saying how this is different. That issue covers local plugin directories being skipped with nothing in the log. The rejection of file-path entries (
configured plugin path must be a directory) is tracked in #46551.Until then, you can keep the root
index.tsre-export, or add"name"and"exports": { ".": "./src/plugin.ts" }to the plugin'spackage.jsoninstead of relying onmain. If your problem is different from those issues, reply here and we can reopen.