Repository navigation
Description
Summary
OpenCode V2 ships two mutually incompatible plugin contracts, and a V1 plugin cannot be made to load on V2 by wrapping its export shape. There is no migration path for plugins that observe runtime activity (tool executions, chat messages, compaction) or register custom tools.
Verified against OpenCode v2.0.22 on Windows.
The break
V1 plugins are an async factory returning a hooks object:
export default async ({ client }) => ({
"tool.execute.after": async (input, output) => { /* ... */ },
})V2 validates the module with Schema.Struct and accepts only two shapes:
// packages/core/src/config/plugin/external.ts
const PluginModule = Schema.Struct({
default: Schema.Union([
Schema.Struct({ id: Schema.String, effect: Schema.declare(...) }),
Schema.Struct({ id: Schema.String, setup: Schema.declare(...) }),
]),
})A V1 module fails to load with:
Plugin must export a default definition with an id and an effect or setup function.
(cause: SchemaError(Expected object at ["def...
Wrapping the export does not help
The migration guide is explicit that the APIs are not translated into each other:
Keep each implementation on its own API; sharing an export does not translate V1 hooks into V2 hooks.
So a shim that adds id + setup() around the V1 factory loads successfully and then silently records nothing. That is worse than the current hard failure: the user believes memory/context capture is running when no hook is ever invoked.
The gap is in the V2 plugin API surface, not the loader
The V2 PluginContext does have equivalents for every V1 hook — this part is fine. What is missing is a way for a V1-shaped plugin to reach them without a rewrite:
| V1 hook | V2 equivalent | available in v2.0.22 |
|---|---|---|
tool.execute.after |
ctx.tool.hook("execute.after") |
yes |
chat.message |
ctx.session.hook("prompt") |
yes |
experimental.chat.system.transform |
ctx.session.hook("context") |
yes |
experimental.session.compacting |
ctx.session.hook("compaction") |
yes |
event |
ctx.event.subscribe() |
yes |
tool (custom tool map) |
ctx.tool.transform() |
yes |
The problem is purely the entry contract: a plugin that only knows V1 cannot register any of the above, because V1 offers no ctx handle to register with. Every affected plugin must be rewritten hook by hook.
Measured ctx keys on v2.0.22:
agent, aisdk, app, command, event, experimental, generate, integration,
location, mcp, model, options, permission, plugin, provider, reference,
rpc, session, shell, skill, storage, tool, vcs, websearch, worktree
Impact
Plugins whose entire purpose is observing runtime activity cannot run on V2 at all. Concretely blocked on v2.0.22:
- claude-mem (thedotmack/claude-mem) — persistent agent memory. Fails to load;
--ide opencodeinstall is unusable. Its MCP tools still work (registered separately), so search works while capture silently does not. - Any plugin built on
experimental.chat.system.transformto inject context into the system prompt. - Any plugin registering custom tools via the V1
toolmap.
This follows the V2 release; the migration guide mentions V1 support but only for the { id, server, setup } dual shape, which requires the plugin author to do the rewrite first.
Suggestions
-
A compatibility path in the loader. When the default export is a function (the V1 shape), call it with a V2-compatible context whose
tool.hook/session.hook/event.subscribe/tool.transformare present, and translate the returned V1 hooks object into registrations. This is a contained change inexternal.tsand would unblock every affected plugin at once. -
A first-class V1 export. Accept
Schema.Struct({ server: Schema.declare(...) })(noid, nosetup) as a documented third union member, warning that only V1 hook names are honored. Cheaper than (1), but each plugin still needs its V1 hook names honored and gets no V2 capability. -
Actionable error message. When the default export is a function, the load error currently reads as if the shape were simply wrong. Naming the V1->V2 rewrite (and linking the migration guide) would save the debugging time I spent here.
Reproduction
// ~/.config/opencode/plugins/v1plugin.js
export default async () => ({ "tool.execute.after": async () => {} })$ opencode run "hi"
Server plugin error
~/.config/opencode/plugins/v1plugin.js
Plugin must export a default definition with an id and an effect or setup function.
Related
- Plugins using the exported v1
{ id, server }module shape fail to load, silently #42878 — plugins using the exported v1{ id, server }module shape - claude-mem issue (upstream): migration blocked on the V1 -> V2 hook rewrite
Activity
opencode-agent commentedon Oct 7, 2026
Thanks for the detailed write-up and the hook mapping table.
This looks like the same request as #48138: a compatibility layer in the V2 loader so V1-style plugins (a default-exported factory returning a hooks object) load, with their hooks mapped onto
ctx.tool.hook/ctx.session.hook/ctx.event.subscribe, plus a clearer error that points to the migration path. Please add your hook mapping (chat.message,experimental.chat.system.transform,experimental.session.compacting, and the customtoolmap) and the claude-mem example there, so everything is tracked in one place.The bot will close this issue as a duplicate of #48138 in 1 day unless you reply explaining how your problem is different.
github-actions commentedon Oct 7, 2026
This issue doesn’t fully meet our contributing guidelines.
What needs to be fixed:
[FEATURE]:.Please edit this issue to address the above within 72 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
If you believe this was flagged incorrectly, please let a maintainer know.
opencode-agent commentedon Oct 8, 2026
Closing this as a duplicate of #48138, since there hasn't been a reply in the last day. That issue asks for the same thing: a compatibility layer so V1-style plugins load on V2 with their hooks mapped onto V2
ctxregistrations, plus a clearer load error. Please add your hook mapping (chat.message,experimental.chat.system.transform,experimental.session.compacting, the customtoolmap) and the claude-mem example to #48138. If your case is actually different, reply here and explain how, and we can reopen. Thanks!