Repository navigation
Description
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
- 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}`
}
},
}- Ask the agent to run
git log --oneline -10. - The bash tool runs
wrapper git log --oneline -10and returns a tool result whosetitleiswrapper git log --oneline -10. - In the next turn the LLM frequently says things like "sorry, I accidentally prefixed the command" — because from its view it typed
git logand the result sayswrapper 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
- Permission rules match rewritten shell commands after tool.execute.before hooks #35882 — permission rules match the rewritten command, breaking user-written allow rules. Same root cause:
tool.execute.beforemutates args invisibly. - Bug:
tool.execute.beforehook arg mutation has no effect #31680 — claims arg mutation has no effect (misdiagnosed; the{ args }object shares the reference so mutation does propagate — that's exactly why Permission rules match rewritten shell commands after tool.execute.before hooks #35882 and this issue exist). - Let tool.execute.before short-circuit a tool call with a substitute result #32565 — asked for
tool.execute.beforeto short-circuit with a substitute result. Same surface, different need. - [FEATURE]: Comprehensive Plugin Pipeline - Middleware-Style Data Flow Control #5148 — broader "plugin pipeline / middleware data flow" feature request.
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
github-actions commentedon Sep 19, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.
Hide the Fence wrapper from opencode's model history