You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
v2: tool.execute.before rewrite executes but chat shows pre-hook input #51596
A server plugin that rewrites shell commands in tool.execute.before (mutating event.input.command, e.g. git status → rtk git status) executes the rewritten command — confirmed in server logs (spawning process ... "rtk git log --oneline -3") — but the chat keeps showing the original pre-hook command ($ git ... instead of $ rtk git ...). In v1 the rewritten command was shown.
Concrete case: the rtk wrapper plugin (https://github.com/rtk-ai/rtk — the v1→v2 format side is tracked at rtk-ai/rtk#3898), OpenCode v2.0.17, Windows 11, PowerShell 7 / Windows Terminal.
Steps to reproduce
Install a server plugin that rewrites event.input.command in ctx.tool.hook("execute.before", ...) (repro: rtk wrapper, git status → rtk git status).
Run any rewritten command, e.g. git status.
The server executes rtk git status, but the chat shows $ git status.
Expected
The chat shows the command that actually ran ($ rtk git status), as in v1.
Adding macOS evidence and narrowing this to the shell tool (opencode v2.0.24, Darwin 25.6.0 arm64, /bin/zsh, latest channel).
Same symptom: a plugin that mutates event.input.command in ctx.tool.hook("execute.before", ...) changes the command that actually runs, but the stored tool call keeps the pre-hook input. Reproduced with both:
the rtk wrapper plugin (git status → rtk git status), and
a project plugin that appends a marker comment (git status → git status # marker).
What may be new: this looks specific to the shell tool, and it regressed relative to bash. In one session I inspected the stored messages via GET /api/session/{id}/context and checked state.input.command for each tool call:
bash tool calls: 19/19 stored inputs contained the injected marker.
shell tool calls: 0/53 stored inputs contained it.
So under bash the execute.before mutation was written back to the message; under shell it is not. Server logs confirm the mutation still reaches execution:
message="spawning process" command=/bin/zsh args=["-c","rtk git status # automatically rewritten ..."]
while the same call's stored tool part is still git status.
Since the bash tool persisted the mutation on the same 2.0.x server, this reads as a shell-tool regression rather than a general hook limitation.
Repro:
Register a plugin that appends a marker to event.input.command when event.tool === "shell".
Run git status (or any command the rtk plugin rewrites).
The server log shows the rewritten/tagged command, but the chat and the stored tool part show the original.
Thanks for the detailed report. I reproduced this on v2.0.17. A project plugin's execute.before hook rewrites the shell command to echo REWRITTEN-BY-HOOK && <command>. The simulated model runs echo original-command. The output shows REWRITTEN-BY-HOOK, so the rewritten command ran, but the chat header still shows $ echo original-command. The recording and screenshot below show it, and the script below reproduces it.
I couldn't confirm it on the latest dev (86ce19d) because my test harness doesn't show tool calls on that build. As far as I can tell from the code, it still runs the post-hook input but shows the model's original input. Related: #37963 (same gap, seen from the model's side), #35882, #42409.
recording.mp4
Reproduction script (drive.ts)
// Repro for anomalyco/opencode#51596: a tool "execute.before" hook rewrites the shell// command; the rewritten command runs, but the TUI still shows the original command.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constplugin=`export default { id: "rewrite-shell", setup: async (ctx) => { await ctx.tool.hook("execute.before", (event) => { const input = event.input as { command?: unknown } if (event.tool === "shell" && typeof input?.command === "string" && !input.command.startsWith("echo REWRITTEN")) { event.input = { ...input, command: "echo REWRITTEN-BY-HOOK && " + input.command } } }) },}`exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},config: {permission: {"*": "allow"}}asany,project: {git: true,files: {".opencode/plugins/rewrite-shell.ts": plugin},},},({ ui, llm })=>Effect.gen(function*(){yield*llm.serve((_request,index)=>index===0
? Stream.make(Llm.toolCall({index: 0,id: "call_shell",name: "shell",input: {command: "echo original-command"}}),Llm.finish("tool-calls"),)
: Stream.make(Llm.text("Done.")),)yield*ui.submit("Run echo original-command")yield*ui.waitFor("Done.",{timeout: 60000}asany).pipe(Effect.catchTag("UiWaitTimeoutError",()=>Effect.void))yield*Effect.sleep(1000)yield*ui.screenshot("rewritten-vs-displayed")}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Activity
github-actions commentedon Sep 27, 2026
This issue might be a duplicate of existing issues. Please check:
tool.execute.beforerewrites make wrapper commands inconsistent in the tool conversation; it discusses how rewritten command details are surfaced to the LLM.jksaunders commentedon Oct 8, 2026
Adding macOS evidence and narrowing this to the
shelltool (opencode v2.0.24, Darwin 25.6.0 arm64,/bin/zsh, latest channel).Same symptom: a plugin that mutates
event.input.commandinctx.tool.hook("execute.before", ...)changes the command that actually runs, but the stored tool call keeps the pre-hook input. Reproduced with both:git status→rtk git status), andgit status→git status # marker).What may be new: this looks specific to the
shelltool, and it regressed relative tobash. In one session I inspected the stored messages viaGET /api/session/{id}/contextand checkedstate.input.commandfor each tool call:bashtool calls: 19/19 stored inputs contained the injected marker.shelltool calls: 0/53 stored inputs contained it.So under
bashtheexecute.beforemutation was written back to the message; undershellit is not. Server logs confirm the mutation still reaches execution:while the same call's stored tool part is still
git status.Since the
bashtool persisted the mutation on the same 2.0.x server, this reads as ashell-tool regression rather than a general hook limitation.Repro:
event.input.commandwhenevent.tool === "shell".git status(or any command the rtk plugin rewrites).opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I reproduced this on v2.0.17. A project plugin's
execute.beforehook rewrites the shell command toecho REWRITTEN-BY-HOOK && <command>. The simulated model runsecho original-command. The output showsREWRITTEN-BY-HOOK, so the rewritten command ran, but the chat header still shows$ echo original-command. The recording and screenshot below show it, and the script below reproduces it.I couldn't confirm it on the latest
dev(86ce19d) because my test harness doesn't show tool calls on that build. As far as I can tell from the code, it still runs the post-hook input but shows the model's original input. Related: #37963 (same gap, seen from the model's side), #35882, #42409.recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.