OpenCode Option 1 (npm): plugin silently skipped on opencode v1.18.31 (no root main + v2 plugin envelope)
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
- Add
main/exportsto the rootpackage.jsonpointing at.opencode/dist/index.js(or publish the entry at the root). - 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. - 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.
Source: affaan-m/ECC