Problem
Plugins that subscribe to tool.execute.before receive output.args and can mutate it, but the tool still executes with the original args from the closure — not the mutated ones.
// tools.ts (before fix)
plugin.trigger("tool.execute.before", { tool: item.id, ... }, { args }) // ← inline object
item.execute(args, ctx) // ← still uses the original args, not the mutated ones
The trigger creates a fresh { args } object each time, so when the plugin mutates output.args.command, it mutates that throwaway object — the original args variable in the closure is never updated.
Root Cause
Three call sites pass an inline object literal { args } to plugin.trigger(), then use the original args for execution instead of reading back from the hook output.
Fix
Capture the hook output in a variable, pass that variable to trigger(), then use hookOutput.args for execution:
--- a/packages/opencode/src/session/tools.ts
+++ b/packages/opencode/src/session/tools.ts
@@ -83,13 +83,14 @@ builtin tools
+ const hookOutput = { args }
const ctx = context(args, options)
yield* plugin.trigger(
"tool.execute.before",
{ tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
- { args },
+ hookOutput,
)
- const result = yield* item.execute(args, ctx)
+ const result = yield* item.execute(hookOutput.args, ctx)
@@ -124,15 +125,16 @@ MCP tools
+ const hookOutput = { args }
const ctx = context(args, opts)
yield* plugin.trigger(
"tool.execute.before",
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
- { args },
+ hookOutput,
)
- return yield* Effect.promise(() => execute(args, opts))
+ return yield* Effect.promise(() => execute(hookOutput.args, opts))
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -288,10 +288,11 @@ TaskTool
+ const taskHookOutput = { args: taskArgs }
yield* plugin.trigger(
"tool.execute.before",
{ tool: TaskTool.id, sessionID, callID: part.id },
- { args: taskArgs },
+ taskHookOutput,
)
- .execute(taskArgs, {
+ .execute(taskHookOutput.args, {
Affected Files
| File |
Call site |
packages/opencode/src/session/tools.ts |
Builtin tools (~L86) |
packages/opencode/src/session/tools.ts |
MCP tools (~L128) |
packages/opencode/src/session/prompt.ts |
TaskTool (~L291) |
Plugins
No response
OpenCode version
1.17.1
Steps to reproduce
Reproduction
Use a plugin that rewrites bash tool commands via tool.execute.before:
export const DemoPlugin: Plugin = () => ({
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
output.args.command = "echo 'intercepted'"
}
},
})
Expected: command is rewritten to echo 'intercepted'
Actual: original command runs unchanged
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Problem
Plugins that subscribe to
tool.execute.beforereceiveoutput.argsand can mutate it, but the tool still executes with the originalargsfrom the closure — not the mutated ones.The trigger creates a fresh
{ args }object each time, so when the plugin mutatesoutput.args.command, it mutates that throwaway object — the originalargsvariable in the closure is never updated.Root Cause
Three call sites pass an inline object literal
{ args }toplugin.trigger(), then use the originalargsfor execution instead of reading back from the hook output.Fix
Capture the hook output in a variable, pass that variable to
trigger(), then usehookOutput.argsfor execution:Affected Files
packages/opencode/src/session/tools.tspackages/opencode/src/session/tools.tspackages/opencode/src/session/prompt.tsPlugins
No response
OpenCode version
1.17.1
Steps to reproduce
Reproduction
Use a plugin that rewrites
bashtool commands viatool.execute.before:Expected: command is rewritten to
echo 'intercepted'Actual: original command runs unchanged
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response