Repository navigation
Description
Summary
The tool.execute.after hook is declared in the Hooks interface in @opencode-ai/plugin but is never invoked anywhere in the OpenCode runtime. Any plugin implementing this hook will have its handler registered but silently ignored — the handler is never called.
Details
Hook declaration
The hook is correctly typed in packages/plugin/src/index.ts:
"tool.execute.after"?: (
input: { tool: string; sessionID: string; callID: string; args: any },
output: {
title: string
output: string
metadata: any
},
) => Promise<void>The plugin.trigger mechanism in packages/opencode/src/plugin/index.ts is also correctly implemented — it passes output by reference, calls all registered handlers, and returns the (possibly mutated) output object:
const trigger = Effect.fn("Plugin.trigger")(function* (name, input, output) {
if (!name) return output
const s = yield* InstanceState.get(state)
for (const hook of s.hooks) {
const fn = hook[name] as any
if (!fn) continue
yield* Effect.promise(async () => fn(input, output))
}
return output
})Missing call site
A search through the entire OpenCode server codebase found zero call sites for plugin.trigger("tool.execute.after", ...):
packages/opencode/src/session/processor.ts— handlestool-resultstream events and callscompleteToolCall(), but no hook triggerpackages/opencode/src/session/llm.ts— assembles AI SDK tools and handles streaming, no hook triggerpackages/opencode/src/tool/registry.ts— only triggerstool.definitionpackages/opencode/src/session/session.ts— no plugin references at allpackages/opencode/src/agent/agent.ts— only triggersexperimental.chat.system.transform- All v2 session files — no hook trigger
Where the trigger should logically live
In packages/opencode/src/session/processor.ts, the tool-result event handler at around line 380 receives the completed tool output and calls completeToolCall(). This is the natural location to trigger tool.execute.after:
case "tool-result": {
// ... EventV2 dual-write ...
// MISSING: yield* plugin.trigger("tool.execute.after", { ... }, value.output)
yield* completeToolCall(value.toolCallId, value.output)
return
}The value.output object has the shape { title, output, metadata } which matches the hook's declared output parameter exactly.
Impact
Plugins that implement tool.execute.after to intercept or modify tool output (e.g., to inject context, compress output, or add telemetry) have no effect. The hook infrastructure is in place but the wire-up is missing.
Proposed Fix
In packages/opencode/src/session/processor.ts, add the plugin trigger before completeToolCall:
case "tool-result": {
const toolCall = yield* readToolCall(value.toolCallId)
// ... existing EventV2 dual-write ...
// Trigger the after hook so plugins can observe/modify tool output
const hookInput = {
tool: value.toolName,
sessionID: ctx.sessionID,
callID: value.toolCallId,
args: toolCall?.part.state.status === "running" ? toolCall.part.state.input : {},
}
const hookOutput = yield* plugin.trigger("tool.execute.after", hookInput, {
title: value.output.title,
output: value.output.output,
metadata: value.output.metadata,
})
yield* completeToolCall(value.toolCallId, { ...value.output, ...hookOutput })
return
}Similarly, tool.execute.before (also declared but never triggered) should be wired up at the tool-call event site.
Also affected
tool.execute.before— declared inHooksbut also never triggered in the runtime.
Environment
- OpenCode version: 1.14.39
- Found via: source audit of
packages/opencode/src/on commit history at time of investigation
Activity
github-actions commentedon May 5, 2026
This issue might be a duplicate of (or conflict with) an existing issue. Please check:
tool.execute.afterhook — but from the opposite direction: that hook does fire for MCP tools, but passes the rawCallToolResult.content[]array instead of the declared{title, output, metadata}shape. The two reports may describe the same underlying issue from different angles, or one may have already been partially fixed. Please review tool.execute.after hook receives raw MCP result instead of assembled output #21149 and clarify whether this is a distinct scenario.If you believe this is a separate issue, it would help to note which commit or version you tested, and whether the behavior differs for native vs. MCP tools.
luismichio commentedon May 5, 2026
Correction after reviewing #21149 and re-auditing
prompt.tsMy initial analysis was wrong. After reviewing the bot's reference to #21149 and doing a targeted search of
packages/opencode/src/session/prompt.ts, I can confirm:tool.execute.afterIS triggered — inprompt.ts, notprocessor.ts.I missed this file in my original audit. Apologies for the incorrect report.
What actually happens
There are two distinct code paths in
prompt.ts, each with different behaviour:Path 1 — Native tools (bash, read, write, etc.) —
L421–L455The hook receives the correct
{title, output, metadata}shape. Mutations tooutput.outputshould be reflected in the returned value. However — crucially — thereturn outputat L451 returns the originaloutputobject, not the result ofplugin.trigger. Sincetrigger()mutatesoutputin-place and returns the same object reference, this does work correctly for native tools.Path 2 — MCP tools —
L465–L491This is exactly what #21149 describes: the hook fires with the raw MCP
CallToolResult(shape:{content: [{type, text}]}) before text assembly happens. Any mutation tooutput.outputby a plugin is ignored because the text is reconstructed fromresult.contentafterward.Conclusion
CallToolResult), assembled output ignores any mutations ❌The two issues together describe the same root cause: MCP tool output is not assembled before the hook fires. The fix for #21149 (assemble text before triggering the hook for MCP tools) would also resolve the mutation-being-ignored scenario.
Impact on context-pipe
Our plugin was attempting to intercept MCP tool results (e.g.,
pipe_read_file). Sincepipe_read_fileis an MCP tool, the hook would have receivedresult.content[0].text— notoutput.output— making the interception incorrect regardless. The plugin has since been updated to a documented placeholder pending the upstream fix.fix(opencode): refactor native plugin to be shape-aware (anomalyco/op…
github-actions commentedon Jul 5, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.