anomalyco / anomalyco/opencode

Legacy plugin loader pushes non-Hooks return values, corrupting plugin loading and crashing startup

Open
#42,451 6 comments 0 reactions 1 assignee View on GitHub

@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) throws Cannot read properties of undefined (reading 'config') (looped under Effect.ignore, so non-fatal but logged).
  • provider state initializer (packages/opencode/src/provider/provider.ts, instanceState.make does for (const hook of plugins) { const p = hook.provider; ... }): undefined.provider throws Cannot read properties of undefined (reading 'provider'). This loop is NOT wrapped in Effect.ignore, so it aborts provider state init and crashes Provider.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

  1. readV1Plugin(mod, "detect") returns undefined when mod.default is absent or is a function (only a record with id/server/tui keys routes to the v1 path). So any module without a record default falls through to the legacy path.
  2. getLegacyPlugins(mod) iterates Object.values(mod) and calls getServerPlugin(entry) (which only checks typeof value === "function"). Every function export is treated as a plugin.
  3. applyPlugin does hooks.push(await entry(input, load.options)) with no check that the returned value is a Hooks object. Helper functions that return undefined or false get pushed into the shared hooks array.
  4. Later consumers index the array as if every entry were a Hooks object. The fatal one is instanceState.make: const p = hook.provider on an undefined entry 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.provider crash.
  • Crash #2 (same minus bmc-panel and server-start-guard): identical 2x j.config + t.provider crash.
  • 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.