Problem
tool.execute.before can mutate a tool's args but can't stop the tool from running, and tool.execute.after only rewrites the result after it ran. There's no way for a plugin to answer a tool call itself and skip execution.
The runtime always runs the tool after the before-hook (packages/opencode/src/session/tools.ts, native path ~L88–93 and MCP path ~L129–139): it triggers tool.execute.before, then unconditionally calls item.execute(...).
Use case: a memory/cache plugin that already holds the answer for a read/grep (an indexed summary or cached result) and wants to return it instead of doing the read. Today it can only let the read happen and prepend a note in tool.execute.after, so the unnecessary work still runs. permission.ask can deny a call but returns an error, not a substitute result.
Proposal
Let tool.execute.before optionally supply a result and/or skip execution:
"tool.execute.before"?: (
input: { tool: string; sessionID: string; callID: string },
output: { args: any; result?: string; skip?: boolean },
) => Promise<void>
After the trigger, if skip is set (or result is provided), build the standard output from result and bypass item.execute(...). Backwards compatible — hooks that set neither behave exactly as today. The native and MCP paths return slightly different output shapes, so the substitute would need handling in both.
Alternatives
tool.execute.after banner — the tool still runs.
permission.ask deny — blocks, but yields an error rather than a clean result.
Happy to open a PR if this seems reasonable. Wanted to check the approach first per the contributing guide.
Problem
tool.execute.beforecan mutate a tool's args but can't stop the tool from running, andtool.execute.afteronly rewrites the result after it ran. There's no way for a plugin to answer a tool call itself and skip execution.The runtime always runs the tool after the before-hook (
packages/opencode/src/session/tools.ts, native path ~L88–93 and MCP path ~L129–139): it triggerstool.execute.before, then unconditionally callsitem.execute(...).Use case: a memory/cache plugin that already holds the answer for a
read/grep(an indexed summary or cached result) and wants to return it instead of doing the read. Today it can only let the read happen and prepend a note intool.execute.after, so the unnecessary work still runs.permission.askcan deny a call but returns an error, not a substitute result.Proposal
Let
tool.execute.beforeoptionally supply a result and/or skip execution:After the trigger, if
skipis set (orresultis provided), build the standard output fromresultand bypassitem.execute(...). Backwards compatible — hooks that set neither behave exactly as today. The native and MCP paths return slightly different output shapes, so the substitute would need handling in both.Alternatives
tool.execute.afterbanner — the tool still runs.permission.askdeny — blocks, but yields an error rather than a clean result.Happy to open a PR if this seems reasonable. Wanted to check the approach first per the contributing guide.