Skip to content

tool.execute.before arg mutation is invisible to the LLM — wrappers confuse the agent #37963

Closed as not planned

Description

@kipyin

Summary

When a tool.execute.before plugin rewrites output.args (e.g. wraps git log as hypa git log), OpenCode feeds the rewritten command to the tool and surfaces it in the tool result (title: input.command in packages/opencode/src/tool/shell.ts), but never tells the LLM that a plugin changed its args. The LLM sees a tool result whose command string doesn't match what it typed, and rationalizes the prefix as its own mistake — e.g. apologizing for an "accidentally prefixed" command that was actually injected by a plugin.

This affects every wrapping-style plugin (hypa, rtk, sandboxes, audit wrappers), not a single plugin. It is a design gap in the tool.execute.before hook contract.

Repro

  1. Install any plugin that wraps bash commands in tool.execute.before:
return {
  "tool.execute.before": async (_input, output) => {
    const command = (output.args as { command?: string }).command
    if (typeof command === "string") {
      output.args.command = `wrapper ${command}`
    }
  },
}
  1. Ask the agent to run git log --oneline -10.
  2. The bash tool runs wrapper git log --oneline -10 and returns a tool result whose title is wrapper git log --oneline -10.
  3. In the next turn the LLM frequently says things like "sorry, I accidentally prefixed the command" — because from its view it typed git log and the result says wrapper git log.

Why it happens

tool.execute.before's output type only exposes args:

"tool.execute.before"?: (input, output: { args: any }) => Promise<void>

There is no channel for the hook to attach a note, annotation, or "rewrite happened" signal that OpenCode forwards into the tool result the LLM sees. Plugins can work around it by also registering tool.execute.after, stashing the rewrite by callID, and mutating output.title/output.output/output.metadata themselves (see opencode-hypa's workaround) — but:

  • every wrapping plugin has to rediscover and reimplement this;
  • the LLM still sees its own original args in the tool-call echo, so the mismatch persists unless the plugin also rewrites the tool-result title;
  • plugins that don't do this (most of them) silently produce the confused-LLM behavior.

Proposal

Pick one (or both):

A. Add an output.note / output.annotations channel to tool.execute.before, appended by OpenCode to the tool result sent to the LLM:

"tool.execute.before"?: (
  input: { tool: string; sessionID: string; callID: string },
  output: { args: any; note?: string },
) => Promise<void>

B. Have OpenCode auto-annotate when output.args differs from input args, e.g. prepend to the tool result:

[tool.execute.before] args rewritten: command "git log --oneline -10" => "wrapper git log --oneline -10"

Option B requires zero plugin changes and covers every existing wrapping plugin. Option A gives plugins control over wording.

Either option should also cover the case where the wrapper throws/short-circuits — today a throw from tool.execute.before surfaces only as an error string with no indication that the original args were modified before the throw.

Related issues

Workaround for current OpenCode

Register tool.execute.after keyed by input.callID, stash rewrites from tool.execute.before, and in the after-hook mutate output.title and output.output to make the rewrite visible. Reference implementation: https://github.com/kipyin/opencode-hypa.

Happy to open a PR for option A or B if either approach sounds acceptable.

Activity

  1. github-actions commented on Sep 19, 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