anomalyco / anomalyco/opencode

[BUG]: a single non-function named export silently disables an entire plugin (no MCP servers, no skills, no user-visible error)

Open
#41,234 3 comments 0 reactions 1 assignee View on GitHub

@nexxeln is already working on this.

Since Aug 8, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Description

Adding one non-function named export to a plugin module makes OpenCode skip the entire plugin. Every MCP server and hook the plugin registers disappears. Nothing surfaces to the user — the plugin simply appears to do nothing.

The error is caught and swallowed, so from the user's side a working plugin and a fatally broken one are indistinguishable.

Reproduce

A v1 plugin that registers an MCP server via the config hook:

// index.js
const dodopayments = async () => ({
  config: async (config) => {
    config.mcp ??= {};
    config.mcp["dodo-knowledge"] ??= {
      type: "local",
      command: ["npx", "-y", "mcp-remote@latest", "https://knowledge.dodopayments.com/mcp"],
      enabled: true,
    };
  },
});

export default dodopayments;

Works. Now add a single named export — anything that is not a function:

export const skills = { id: "x", setup: async () => {} };
Result
module exports MCP servers registered log
default (function) yes
default + skills (object) none Plugin export is not a function

Verified on 1.18.15, deterministic across repeated runs, isolated HOME.

Important: the plugin must be tested at the path OpenCode actually resolves — ~/.cache/opencode/packages/<pkg>/node_modules/<pkg>/.... Installing into the project's node_modules tests nothing, because OpenCode fetches from the registry into its own cache. That cost me a full round of invalid results.

Cause

readV1Plugin(mod, spec, "server", "detect") returns undefined because mod.default is a function rather than a { id, server } record. Loading falls through to getLegacyPlugins(mod), which iterates Object.values(mod) and throws Plugin export is not a function for any export that is neither a function nor an object with a callable server. applyPlugin then rejects before pushing anything to hooks, and the surrounding Effect.tryPromise(...).pipe(Effect.catch(...)) swallows it.

So the blast radius of one stray export is the whole plugin, not just that export.

Why this is easy to hit

Plugin authors adding v2 support naturally reach for a named export alongside the existing default — the v2 shape is { id, setup }, which reads like something you would export by name. That is exactly the shape that triggers this. The two APIs are mutually exclusive at the entrypoint (v2 wants default to be the { id, setup } object, v1 wants default to be the plugin function), which is not obvious, and the failure gives no hint.

Suggested fix

Either would remove the footgun:

  1. Skip exports that are not plugin-shaped instead of throwing — a module is allowed to export types, constants, or helpers.
  2. If throwing is intended, surface it: a plugin that fails to load should be visible to the user, not only a swallowed log line. Silent total failure is the worst outcome here.

(1) alone also makes forward-compatible dual-API modules possible.

Related
  • #20940 — plugin config() mutations to skills.paths invisible to discovery. Auto-closed as stale rather than fixed; the closing bot invites a new issue. Still reproduces for me on 1.18.15, and it is why a bundled skills/ directory cannot be self-registered.
  • #33896 — v2 ctx.skill.transform() skills not discoverable.

Together these mean an npm-distributed plugin currently has no supported way to ship Agent Skills: the config() route does not register them, the v2 route is not discoverable, and adding the v2 export as a named export triggers the bug above. The workaround is asking every user to add skills.paths to their own opencode.json by hand.

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.