anomalyco / anomalyco/opencode
Legacy plugin loader pushes non-Hooks return values, corrupting plugin loading and crashing startup
@jlongster is already working on this.
Since Aug 14, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
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 #1 (load list: magic-context, instant-file-search, mullvad, server-start-guard, bmc-panel, speak, gk-hooks): 2x
j.config+t.providercrash. - Crash #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 76c631d198f9ff620e15468e45f3457d50481b57
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.