Skip to content

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

Closed as not planned

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

Activity

  1. github-actions commented on Jun 10, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Please review these issues and consider closing this one if it is already covered.

  2. github-actions commented on Aug 10, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions