#3127·ECC

OpenCode Option 1 (npm): plugin silently skipped on opencode v1.18.31 (no root main + v2 plugin envelope)

Author: yanhenrique-devCreated Sep 15, 2026Updated Sep 15, 2026

Bug: OpenCode Option 1 (npm) — plugin is silently skipped on opencode v1.18.31

Following .opencode/README.md Option 1 does not load the plugin on current opencode (v1.18.31, released 2026-09-14). Nothing errors; hooks and custom tools are simply absent.

Environment

  • [email protected] (latest)
  • opencode v1.18.31 (AppImage, linux)
  • Config: "plugin": ["ecc-universal"]

Root cause 1 — no main/exports in the published root package.json

packages/core/src/npm.ts (resolveEntryPoint) resolves npm plugin specs via import.meta.resolve(name, dir). The published root package.json of ecc-universal declares no main and no exports, and there is no index.* at the package root — the actual entry point lives at <pkg>/.opencode/dist/index.js (declared only in the nested .opencode/package.json, which node resolution ignores).

So resolution throws, entrypoint is undefined, and packages/core/src/config/plugin/external.ts bails early:

const entrypoint = path.isAbsolute(ref.package)
  ? pathToFileURL(ref.package).href
  : (yield* npm.add(ref.package)).entrypoint
if (!entrypoint) return   // <-- silent skip

Verified locally:

$ node -e "import('ecc-universal').then(...)"
FAIL: ERR_MODULE_NOT_FOUND Cannot find package '.../ecc-universal/index.js'
$ node -e "import('ecc-universal/.opencode/dist/index.js').then(...)"
OK keys: [ 'default' ]

Root cause 2 — default export is a v1 promise-style function, not the v2 {id, setup} envelope

Even with the correct entrypoint, external.ts decodes the module namespace:

const PluginModule = Schema.Struct({
  default: Schema.Union([
    Schema.Struct({ id: Schema.String, effect: ... }),
    Schema.Struct({ id: Schema.String, setup: ... }),
  ]),
})
...
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default

.opencode/dist/index.js re-exports ECCHooksPlugin, which is a bare async ({ client, $, directory, worktree }) => Hooks — no id, no setup/effect. The decode fails and the failure is swallowed by Effect.ignoreCause, so nothing loads:

DECODE FAILS: Expected object, got async ({ client, $, directory, worktree, }) => {

Beyond that, the v2 promise contract (setup(context) => void with agent/command/skill/catalog/integration/reference domains) has no way to register tool.execute.before/after, file.edited, permission.ask, shell.env, or custom tools — so a simple wrapper is not enough; the plugin needs to be authored against the v2 effect API to expose the advertised hooks/tools.

Suggested fixes

  1. Add main/exports to the root package.json pointing at .opencode/dist/index.js (or publish the entry at the root).
  2. Export a v2 plugin: { id: "ecc-universal", effect } (effect variant) so the hooks map and custom tools can be registered on the current host — or document that Option 1 targets opencode <= 1.17.
  3. In the meantime, the catalog (commands/skills/agents) loads fine when copied into ~/.config/opencode/{commands,skills,agents}/ — only hooks/tools are affected.

Happy to send a PR if the v2-effect migration is wanted.