Repository navigation
Description
Summary
When a tool.hook("execute.before") plugin hook throws for several tool calls issued in one parallel batch, every failing call surfaces the first thrown error's message. The distinct errors thrown by the other calls' hooks are discarded. Non-blocked calls in the same batch are unaffected. This misleads both the agent and the user about which tool call was rejected and why.
Environment
- opencode version: 2.0.11
- OS: Windows_NT 10.0.26200 (win32 x64)
- Terminal: TERM=xterm-256color (TERM_PROGRAM unset)
- Shell: Git bash (
shell: "C:\Program Files\Git\bin\bash.exe"in config); ComSpec is cmd.exe - Install/channel: latest (autoupdate: notify)
- Active plugins: local project plugin under
.opencode/plugins/(auto-discovered) +@plannotator/opencode@latest(global)
Reproduction
Minimal plugin (.opencode/plugins/hook-error-repro.js; run npm i @opencode/plugin inside .opencode/ if the package is not resolvable yet):
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "hook-error-repro",
async setup(ctx) {
await ctx.tool.hook("execute.before", (event) => {
const cmd = event.input?.command ?? ""
if (cmd.includes("alpha-block")) throw new Error("HOOK BLOCK ALPHA")
if (cmd.includes("beta-block")) throw new Error("HOOK BLOCK BETA")
})
},
})- Have the model issue two
shelltool calls in one parallel batch (two tool-use blocks in a single assistant message):- call A:
echo alpha-block - call B:
echo beta-block
- call A:
- Observe the two tool results.
Expected: A fails with HOOK BLOCK ALPHA, B fails with HOOK BLOCK BETA.
Actual: both results fail with HOOK BLOCK ALPHA. The second hook's distinct error never reaches the tool result. The same happens with any mix: with N parallel calls where multiple hooks throw, all failures show the first throw's message. A benign call in the same batch (e.g. echo ok) executes normally, so error attribution is per-call for successes but not for hook failures.
To verify that the plugin really throws distinct errors (i.e. the loss happens in core, not in the plugin), append a random nonce to each thrown message and log the hook inputs server-side:
- the hook received the correct, distinct command for each call,
- the plugin threw two different errors (two different nonces),
- both tool results still displayed the nonce of the first throw.
Expected Behavior
Each failing tool call in a parallel batch surfaces the error thrown by its own execute.before hook invocation, the same way natural execution failures are attributed per call (two parallel calls to a missing file return two distinct "File not found" errors — only hook errors collapse).
Actual Behavior
All failing tool calls in the batch display the first hook error's message. Observed consistently across multiple batches (first-error-wins, not last-error-wins).
Additional Context
- Found while testing a tool call guarding plugin (a local project plugin that blocks dangerous shell commands): with four parallel shell calls matching four different block patterns, every result reported the first command's name and reason. Confirmed with an instrumented plugin that per-call hook inputs and thrown errors were correct, so the collapse is in core's parallel hook-error attribution (observed on the server->tool-result path; not distinguished whether the latch is in the server or the client result pipeline).
- Not specific to shell: any tool gated by
execute.beforeshould be affected; verified with shell in this session. - Workaround status: none found at plugin level (throwing distinct errors does not help; message content is overwritten, not deduplicated).
Activity
1052326311 commentedon Sep 21, 2026
Reproduced on current v2 (
b8aa08f26) through the Promise plugin adapter and session runner: two hooks throw ALPHA/BETA, but both persisted tool results contain ALPHA; a successful sibling still runs. I am preparing a narrow fix to retain each failed call's own cause during settlement, with coverage for the next model request and unchanged decline/interruption behavior.graphenn commentedon Oct 13, 2026
Still reproduces on opencode 2.0.27 (Linux x64), with a project plugin that throws distinct
execute.beforeerrors for blocked shell commands — same shape as the original report:shellcalls in one parallel batch → both results carry the first thrown error's message; the second call's own message is lostreads of missing files) → attributed per call, unaffected (tool.executiontype)Verbatim evidence (two blocked calls A and B in one batch; message excerpts are from a Chinese-UI guard plugin, the matched command excerpt is the relevant part):
bin/gcube dev-local enable→...(命中:bin/gcube dev-local enable)(correct)bin/gcube obs mirror --tier smoke --instance dev-local --yes→ error message also shows(命中:bin/gcube dev-local enable)(A's excerpt)No plugin-side workaround found: the hook computes a fresh, distinct message per invocation (verified by construction), and the duplication happens after the hook throws.