Skip to content

Bug: tool.execute.before hook arg mutation has no effect #31680

@panudetjt

Description

@panudetjt

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions