Repository navigation
Description
Summary
opencode 1.16.2's legacy plugin loader (getLegacyPlugins in packages/opencode/src/plugin/index.ts) invokes EVERY exported function of a plugin module as a plugin and pushes its return value into the hooks array WITHOUT validating it is a Hooks object. A plugin that exports helper functions that return undefined/false (in addition to the real plugin factory) pollutes the hooks array with non-Hooks entries. This corrupts plugin loading and can crash startup without a visible load error.
The same root cause explains two crash signatures observed at startup:
- config-hook loop:
(hook as any).config?.(cfg)throwsCannot read properties of undefined (reading 'config')(looped underEffect.ignore, so non-fatal but logged). - provider state initializer (
packages/opencode/src/provider/provider.ts,instanceState.makedoesfor (const hook of plugins) { const p = hook.provider; ... }):undefined.providerthrowsCannot read properties of undefined (reading 'provider'). This loop is NOT wrapped inEffect.ignore, so it aborts provider state init and crashesProvider.list.
Context
Custom plugins written as { id: <plugin>, ...hooks } legacy-style modules, or plugin modules (built with the @opencode-ai/plugin SDK) that chose named exports: the plugin factory plus adjacent helper functions. Example: a plugin module exporting isLockdownError, handleRateLimit, timerTick, and <Plugin> as four named function exports. The legacy loader treats all four as plugin factories.
Root cause detail
readV1Plugin(mod, "detect")returnsundefinedwhenmod.defaultis absent or is a function (only a record withid/server/tuikeys routes to the v1 path). So any module without a recorddefaultfalls through to the legacy path.getLegacyPlugins(mod)iteratesObject.values(mod)and callsgetServerPlugin(entry)(which only checkstypeof value === "function"). Every function export is treated as a plugin.applyPlugindoeshooks.push(await entry(input, load.options))with no check that the returned value is a Hooks object. Helper functions that returnundefinedorfalseget pushed into the shared hooks array.- Later consumers index the array as if every entry were a Hooks object. The fatal one is
instanceState.make:const p = hook.provideron anundefinedentry throws.
Expected behavior
A plugin that returns a non-Hooks value from its module export should fail that plugin load gracefully and log a clear error, rather than polluting the hooks array and crashing a later consumer. At minimum, getLegacyPlugins / applyPlugin should filter out entries whose value is not a Hooks object.
Impact
- Crash feat: compact and other improvements #1 (load list: magic-context, instant-file-search, mullvad, server-start-guard, bmc-panel, speak, gk-hooks): 2x
j.config+t.providercrash. - Crash Roadmap & Existing Issues #2 (same minus bmc-panel and server-start-guard): identical 2x
j.config+t.providercrash. - Clean boot (crashing plugin removed): zero errors.
The single discriminating variable between the crashing boots and the clean boot was the plugin with the helper exports returning undefined.
Environment
- opencode 1.16.2 (WinGet install)
- @opencode-ai/plugin SDK 1.17.8
- Loader ref: tag v1.16.2 = commit 76c631d
Activity
github-actions commentedon Aug 14, 2026
This issue might be a duplicate of existing issues. Please check:
TypeError: Cannot read properties of undefined (reading 'provider')crash inProvider.list()on startup — the same crash signature described here, though that report attributed it to the Desktop sidecar rather than the plugin loader. The root cause you've identified here may explain that issue as well.MilkyWay008 commentedon Aug 14, 2026
Had this exact crash after a plugin update a few weeks back. Easiest way to get back up: rename the plugin folder at
%USERPROFILE%\.config\opencode\plugin\to something else, boot opens clean, then add plugins back one at a time to find the offender. Your root cause reads right to me, any named function export gets treated as a plugin factory, so keeping helper exports in a separate module fixes it at the source. Oh, btw, in case it might help that, I built a portable Hermes OTG repo, check it, imo it may be able to help you diagnose and figure out the fix in no time. https://github.com/MilkyWay008/Hermes-OTGEv3lynx727 commentedon Aug 17, 2026
Same root cause, third symptom — confirmed on v1.18.9 (Linux x64).
Symptom:
failed to load plugin @dietrichgebert/ponytail error="path must be a string or a file descriptor"at startup.Trace: the ponytail module exports the plugin as
default(a plain function, soreadV1Plugin(..., "detect")misses it) plus a named helperparseCommandFile(filePath).getLegacyPluginscalls every function export with the PluginInput object →parseCommandFile(input)→fs.readFileSync(input)→TypeError: path must be a string or a file descriptor. The entire plugin silently fails to load; the error gives zero hint that a non-plugin export is the culprit.Confirming evidence for this issue: same "invoke every export" behavior, surfacing as a raw fs TypeError instead of the
undefined.config/undefined.providercrashes. Repro is trivial — any legacy plugin module that also exports a named helper function that touches fs.Possible fix direction: in
getLegacyPlugins, only treat thedefaultexport (or exports matching an explicit plugin contract) as plugins and skip the rest, plus wrap invocations so the error names the offending export. A plugin author who exports utilities alongside the plugin is a legitimate pattern and shouldn't be a footgun.Ghilteras commentedon Aug 21, 2026
Confirming on 1.18.19, with the third crash site
Hit this today on 1.18.19 (latest release) and it took out an entire agent fleet. Adding our
data because the root cause in this issue is exactly right and the code is unchanged on
dev.Trigger. One plugin file exported its
Pluginfactory plus four plain helper functions — thehelpers were exported only so a unit test could import them:
getLegacyPluginscalls all five with(PluginInput, options), sohooksends up as[object, boolean, string, undefined, string].Symptoms. All three guarded consumers logged, and then message creation died:
That last one is the fourth unguarded site, on top of the
provider.tsone already in thisissue:
Plugin.trigger(packages/opencode/src/plugin/index.ts:291-294) doesfor (const hook of s.hooks) { const fn = hook[name] ... }with no null check, so no sessioncould create a user message at all. Not degraded — zero messages.
Why this is worse than it looks. The visible failure was at the model call, so we spent the
outage debugging our model provider. The plugin errors are
Effect.ignored and only reachable bygrepping the log; nothing connects them to "sessions cannot send messages". Deleting the four
helper exports fixed it completely, with no other change.
Reproduces deterministically with the loader semantics extracted from
dev:Also worth noting for whoever picks this up: PR #35489 fixed the non-function half of this and was
auto-closed by the stale-PR bot on 2026-08-07, not rejected. I have a patch that covers both this
issue and #41234 and will open it as a PR.
Opened #43785 with a fix for this (and #41234): each export is validated independently — non-function exports, factories that throw, and factories returning a non-object are skipped with a warning naming the export and the reason, while a module with no usable plugin export still throws so genuinely broken plugins stay loud.
sitex commentedon Aug 29, 2026
Confirmed on OpenCode 1.18.25, Linux x64.
A local plugin exported its default factory plus one named helper used by tests. Normal
opencodestartup logged:The TUI surfaced only
Unexpected server error.opencode --pureopened normally with the same provider/model config.Toggle proof: with the named helper export present,
opencode models cliproxyapiexited 1 onn.provider; making only that helper module-private listed all 7 configured models, and the exactopencodecommand rendered the TUI. Restoring the export reproduced the crash; removing it restored startup.The workaround is to expose only the default plugin factory. We also changed the test to obtain the helper through the returned
tool.execute.beforehook and assertObject.keys(pluginModule)equals["default"]. This remains reproducible becauseapplyPlugin()on currentdevstill pushesawait server(...)without validating the returned Hooks object.mike-ildan commentedon Aug 30, 2026
Adding a fourth crash site and a published-package impact. Not re-confirming the version, since @sitex already covered 1.18.25 above, and not restating the root cause, which the issue body already gets right.
The
Plugin.triggersite, which fires mid-session rather than at startupThe consumers listed so far are the
configloop, thedisposeloop, andProvider.list/ provider state init. There is one more, and it is the one that reaches the user as a broken prompt rather than a broken boot.packages/opencode/src/plugin/index.ts#L292, insidePlugin.trigger:if (!fn) continueguards the hook's property but never the hook itself, so anullentry throws on indexing. Captured on 1.18.25:Startup completes normally here. The session opens, the model is listed, and the failure lands on the first prompt, which sends people looking at their provider or their network rather than at a plugin. Worth noting because it means the blast radius is not limited to boot.
Also a small addition to the reported shapes: every example in the thread so far has a helper returning
undefinedorfalse. This one returnsnull, from ordinary lookup helpers (a tool-name translation that finds no match, a parse that does not match).nullis the common case in real code, and no argument guard can prevent it, which is worth weighing against any fix that only hardens the call.A published plugin is fully broken by this
opencode-claude-bridge@1.10.12, which is the currentlateston npm, does not load at all on 1.18.25. It exports six ordinary helpers alongside its default, two of which throw when called with the plugin input, sogetLegacyPluginsaborts the whole file:Filed there as dotCipher/opencode-claude-bridge#27 with a fix. Mentioning it here only as evidence that this reaches past locally-authored plugins into published ones, where users cannot see the cause and have no workaround short of editing
node_modules.Minimal repro
{ "plugin": ["file:///abs/path/p.ts"] }opencode run "hi"→ threeplugin config hook failed, then the fatalz[W]. Startup is clean; only the prompt fails.One diagnostic note for anyone bisecting:
opencode debug configdoes not load plugins, so it reports success for a plugin that cannot load.opencode debug infodoes.On the fix
The issue body's suggestion (filter non-Hooks entries at
applyPlugin) would close all four sites at once, and is the right layer. If a narrower change is preferred,L292needs its own guard regardless, sinceif (!fn)does not cover a nullhook.Analysis was AI-assisted. Every log line and stack above is captured output from an actual run, not reconstructed; the source line references are against the current default branch.
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊