Repository navigation
Empty bash tool arguments {} can still be submitted and interrupt the session #27871
Description
Description
A bash tool call with completely empty arguments ({}) was submitted from an OpenCode session and caused the tool execution to be interrupted. This looks related to the previously reported empty-input / aborted-tool family of bugs, but it is still observable on OpenCode 1.15.2 and with openai/gpt-5.5.
The problematic tool call shape was:
{
"recipient_name": "functions.bash",
"parameters": {}
}For the bash tool this is invalid because required fields such as command, workdir, and description are absent. The runtime/tool layer should reject this before execution with a clear validation error and return control to the user, rather than allowing the session to enter an interrupted/stuck state.
What happened
During a normal assistant session, the assistant attempted to run a parallel tool batch that included:
- A valid
readcall for a JSON config file. - An invalid
bashcall whose parameters were{}.
The UI showed the read result, then the invalid bash tool reported:
[Tool execution was interrupted]
From the user perspective, this made the current OpenCode session difficult to exit/recover from. The user explicitly reported that this kind of invalid empty-argument tool call has caused them to be unable to exit OpenCode cleanly in prior sessions as well.
Concrete transcript evidence
The assistant later acknowledged the exact invalid tool call as:
{"recipient_name":"functions.bash","parameters":{}}The current session search also finds the same call shape in the session history:
session: ses_1cf21f371ffe4YhyJkDKmeU31Q
match: {"recipient_name":"functions.bash","parameters":{}}
The tool call was produced inside a multi_tool_use.parallel wrapper. The valid sibling call completed, while the invalid bash call had no arguments at all.
Expected behavior
OpenCode should guard against empty arguments for tools with required parameters.
For bash, if input is {} or missing required fields, OpenCode should:
-
Not start tool execution.
-
Fail the tool call immediately with a schema/validation error such as:
bash tool call rejected: missing required field `command` -
Return control to the user cleanly.
-
Avoid leaving the session in an interrupted, half-running, or unrecoverable state.
-
Ideally provide model-facing feedback so the assistant can retry with a valid tool call or stop.
Actual behavior
The invalid bash tool call was submitted with empty arguments:
"parameters": {}The session then showed:
[Tool execution was interrupted]
The user experienced the session as stuck / hard to exit.
Why this matters
This is especially harmful because {} is not a subtly malformed bash call. It is obviously invalid for the tool schema. Users should not have to recover manually from a model/tool-stream failure that could be rejected before execution.
This also makes the behavior look intentional or malicious to users, because the invalid call is visibly empty and the resulting session becomes hard to control.
Environment
- OpenCode version:
1.15.2 - Model:
openai/gpt-5.5 - OS/session: Linux, KDE Plasma, Wayland
- Shell: bash
- Harness/plugins: OhMyOpenCode / Sisyphus-style tool orchestration was active
- Tool path involved:
multi_tool_use.parallelcontaining afunctions.bashchild call
Related issues / PRs
This appears related to the known empty-tool-input and aborted-tool-call issue family:
- Windows: aborted bash tool call can leave a dangling tool_use without tool_result and corrupt the session #21489:
bashtool recorded withinput: {},raw: "",Tool execution aborted, then session corruption. That issue was Windows/OpenCode1.4.0; this report is Linux/OpenCode1.15.2. - Tool execution aborted when tool call is truncated (empty input) #13102: tool call truncated with empty input
{}causingTool execution aborted. - Bash tool called with invalid arguments: "expected string, received undefined" for command and description fields #14519:
bashcalled with invalid arguments,command/descriptionreceivedundefined. - Truncated tool calls are misclassified and unrecoverable (finishReason: length + repairToolCall + doom loop) #18108: truncated tool calls misclassified/unrecoverable, including
bashas a possible affected tool. - Infinite retry loop when StreamIdleTimeoutError occurs during tool input generation #12233: repeated empty tool inputs after stream timeout during tool input generation.
- [Bug] GPT-5.4 repeatedly sends empty apply_patch tool calls (
{}), causingTool execution aborted, while GPT-5.3 Codex does not #20227: GPT model repeatedly sends emptyapply_patch {}calls. - fix(session): detect empty tool args from gpt-5.5 and prevent abort loop #25511: PR titled
fix(session): detect empty tool args from gpt-5.5 and prevent abort loop.
Suggested fix direction
Add a generic pre-execution guard for all tool calls:
-
If a tool has required parameters and the input is
null,undefined, or{}, reject before execution. -
Surface a structured validation error to both the UI and the model.
-
For batched/parallel tool calls, fail only the invalid child call and keep the parent session recoverable.
-
Add regression coverage specifically for:
{ "tool": "bash", "input": {} } -
Verify the same guard applies when the empty input originates from
multi_tool_use.parallel/ batched tool execution, not only direct tool calls.
Minimal reproduction shape
I do not have a deterministic model-only repro, but the runtime should be able to unit-test this directly by simulating a tool call equivalent to:
{
"tool": "bash",
"input": {}
}or, through the parallel wrapper:
{
"tool": "multi_tool_use.parallel",
"input": {
"tool_uses": [
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "/some/file.json",
"offset": 0,
"limit": 200
}
},
{
"recipient_name": "functions.bash",
"parameters": {}
}
]
}
}The expected outcome is a clean validation failure for the second child call, not a tool interruption or session-level recovery problem.
Activity
github-actions commentedon May 16, 2026
This issue might be a duplicate of existing issues. Please check:
{}→ "Tool execution aborted"{}), causingTool execution aborted, while GPT-5.3 Codex does not #20227: GPT model repeatedly sends empty tool calls ({}), causing "Tool execution aborted" — very similar behavior with a different tool (apply_patchvsbash)bashtool,input: {}, "Tool execution aborted"If you believe this is a distinct new case, please clarify how it differs from the issues above.
eatFirewood commentedon May 16, 2026
Additional forensic detail / correction after inspecting the local OpenCode DB and logs:
I want to refine the original wording. The strongest evidence does not show that the model completed a
bashtool call with literal{}arguments. It looks more like OpenCode created/persisted abashtool-call part and then the stream/runtime was interrupted before any argument payload was captured.Local DB evidence
The exact
partrow for the problematic bash tool call is:{ "type": "tool", "tool": "bash", "callID": "call_yeC2JnBpLjawqbv2JpFy3wNB", "state": { "status": "pending", "input": {}, "raw": "" } }Important details:
statusispending, notcompletedorerror.inputis{}.rawis an empty string"", not the string"{}".That distinction matters. If the model had fully emitted empty JSON args, I would expect something closer to
raw: "{}"followed by schema validation failure. Instead, this looks like a tool-call shell was created, but argument streaming never completed.The same assistant message has no normal finish/token accounting:
{ "id": "msg_e30e99482001wX4eo84q5SKVK2", "modelID": "gpt-5.5", "providerID": "openai", "tokens": { "input": 0, "output": 0, "reasoning": 0, "cache": { "read": 0, "write": 0 } } }It also lacks a normal
finishvalue.Neighboring parts in the same assistant message
The message parts show this sequence:
step-startempty
reasoningpartuser-visible commentary text:
a completed
readtool call with valid input/output:{ "type": "tool", "tool": "read", "state": { "status": "completed", "input": { "filePath": "/home/wt/.config/io.github.clash-verge-rev.clash-verge-rev/window_state.json", "offset": 0, "limit": 200 } } }the pending
bashtool call with emptyinputand emptyrawshown above.Runtime log evidence
Around the same time, after the
readtool completed, the OpenCode log shows:This suggests a runtime/worker-side failure occurred while the assistant message/tool batch was in progress.
Updated hypothesis
The likely sequence is:
The assistant began a response.
It emitted commentary saying it would stop the Clash Verge UI process and edit
window_state.json.It successfully executed a
readcall forwindow_state.json.A
bashtool-call part was created/persisted.Before any bash arguments were captured, the OpenCode runtime/worker hit
InstanceRef not provided rejectionand then worker shutdown failed.The session was left with a pending bash tool part:
{ "tool": "bash", "state": { "status": "pending", "input": {}, "raw": "" } }So this may be less "model deliberately emitted
{}" and more "runtime interrupted during tool-call assembly, leaving a half-created tool call with default empty input".Why this still belongs in the empty-input/tool-interruption family
Even if the model did not intentionally emit
{}, the stored session state still contains abashtool part withinput: {}and no raw args. This is the exact state that later becomes confusing/unrecoverable for the user and can be rendered as an empty-argument tool call.Suggested additional fix direction
Besides validating
{}before tool execution, OpenCode should probably also guard/clean up this intermediate state:If a tool part is still
pendingwithraw: ""when the assistant stream/worker fails, mark it as an interrupted error with a synthetic tool result or remove it from model-visible history.Preserve the underlying runtime error (
InstanceRef not provided) in the part metadata/error so the UI shows the true cause instead of making it look like the model intentionally calledbashwith{}.Add regression coverage for a partially-created
bashpart:{ "type": "tool", "tool": "bash", "state": { "status": "pending", "input": {}, "raw": "" } }Ensure worker/runtime errors during tool-call streaming always leave session history in a recoverable state.
eatFirewood commentedon May 16, 2026
Additional clarification: this appears to be two separate bugs that combine into the user-visible failure.
1. Triggering bug: the stream/worker can fail while a tool input is only partially assembled
The local evidence suggests the
bashpart was created aftertool-input-start, but the stream failed before the latertool-callevent that would carry the completed arguments.That leaves this intermediate state:
{ "type": "tool", "tool": "bash", "state": { "status": "pending", "input": {}, "raw": "" } }In this state,
input: {}is not necessarily a completed model-emitted empty argument object. It is the processor's initial placeholder for a tool input that has started streaming but has not reached the executabletool-callphase.The surrounding runtime log showed:
So the likely trigger is an upstream worker/runtime/context failure while the tool input stream was in progress. Subagents/background jobs can amplify this class of failure because a task tool is itself an agent loop with additional parent/child session lifecycle and background job state, but the first observed
pending bash input:{} raw:""incident happened in the parent session before the later nested plan/explore/librarian subagents were launched.2. Recovery bug: SessionProcessor can treat an incomplete pending tool as something worth waiting for
The second bug is independent and is the safer one to fix directly in the session processor.
The relevant state machine is:
A
pendingtool is not executable. It has no completed arguments and no tool executor has started. Only arunningtool can later produce a tool result/error.In the implementation, the deferred for a tool call is created at
tool-input-start, but it is only settled bycompleteToolCall/failToolCall; both paths require the stored part to berunning. Therefore, if the stream fails aftertool-input-startbut beforetool-call, awaiting that deferred is a guaranteed dead wait: no future code path can settle it.This is why a timeout is not the right fix. Tool runtime duration is inherently unbounded; a slow
bash/test/build command may be legitimate. The precise condition is not elapsed time. The precise condition is state:running: tool has complete input and started execution, so cleanup may wait for the result.pending: tool input was never completed, so cleanup must not wait for an execution result that cannot exist.Suggested precise fix
In
SessionProcessor.cleanup, only wait for unresolved tool calls whose persisted part is stillrunning. Do not wait forpendingtool parts. Let the existing cleanup path convert remaining tool parts into an interrupted error so model-visible history remains recoverable.Pseudo-shape:
Regression test shape
A focused regression test can simulate:
LLM stream emits
tool-input-startforbash.LLM/worker stream fails before emitting
tool-call.SessionProcessor.process()returns/stops instead of hanging.The stored tool part is converted from:
{ "status": "pending", "input": {}, "raw": "" }to an interrupted error with
metadata.interrupted === true.This addresses the session corruption/hang even if the upstream
InstanceRef/ worker-context failure is fixed separately later.eatFirewood commentedon May 16, 2026
Additional finding: there is another reproducible-looking trigger in the same stuck-session family. This one does not require an empty
bash {}call. It happens after a normalreadtool call completes and the read tool starts its LSP warm-up side effect.I am adding it here because it explains why the original symptom can be frequent: a common read operation can trip a runtime/context defect, and then the session processor may leave the assistant message in an unfinished state.
Sanitized triggering sequence
During a normal code-analysis turn, the assistant started responding and read an OpenCode TypeScript source file:
{ "type": "tool", "tool": "read", "state": { "status": "completed", "input": { "filePath": "packages/opencode/src/session/processor.ts", "offset": 620, "limit": 110 } } }Immediately after that successful
read, the log showed:The corresponding assistant message was left with zero token accounting and no normal finish/error:
{ "role": "assistant", "modelID": "gpt-5.5", "providerID": "openai", "tokens": { "input": 0, "output": 0, "reasoning": 0, "cache": { "read": 0, "write": 0 } } }So this failure mode is not "the model emitted an empty bash call". The completed tool in this case was a valid
read. The session still became stuck because a background/runtime side effect failed after the tool completed.Why LSP is involved
readis not only a file-read operation. After producing the file output, it also warms LSP:lsp.touchFile()needs an instance-scoped context:and
InstanceState.contextcurrently defects ifInstanceRefis absent:This means a best-effort background optimization attached to
readcan surface as a runtime rejection instead of being contained as a normal tool error or skipped LSP warm-up.Why this matters for the original issue
The original report focused on a persisted pending
bashpart with:{ "status": "pending", "input": {}, "raw": "" }That is still a real recovery/state-machine bug. But the newer evidence shows a broader pattern:
finish,error, or completed cleanup state.This also explains the frequency: reading source/config/log files is common, and
readcan trigger LSP warm-up even though the user-visible tool call itself is valid.Suggested fix direction
I think this should be fixed at two boundaries.
First, make
read's LSP warm-up truly best-effort:readshould return successfully even if LSP warm-up cannot run.InstanceRefduring a non-essential warm-up should be skipped or logged, not raised as a runtime defect.InstanceRef, capture/provide it explicitly before forking the warm-up effect, or make this specific warm-up path no-op when no instance context is available.Second, keep the session processor resilient to runtime defects:
finish, noerror, and no completion timestamp.pending + input:{} + raw:"") and stale running tools should be converted into explicit interrupted/error states rather than left as waitable work.Regression coverage
Useful regression tests would be:
readtool call wherelsp.touchFile()defects withInstanceRef not provided; expected result: the read output is still returned and no unhandled rejection escapes.readis followed by a runtime-side LSP defect; expected result: the session exits cleanly and the assistant message is not left half-created.tool-input-startfires but the stream fails beforetool-call.If maintainers prefer, this LSP/context issue can be split into a separate issue. I am adding it here because it appears to be another trigger for the same user-visible stuck-session behavior.
InCerryGit commentedon Jul 8, 2026
Additional evidence from OpenCode
1.17.15showing the same stuck-session family is still observable on the current release.This is intentionally sanitized: project names, local paths, repository names, and real session/message/call IDs are replaced with placeholders.
Environment
1.17.15openai/gpt-5.5Observed symptom
A long-running session stopped making provider/API requests and appeared stuck. Inspecting the local OpenCode SQLite state showed that the session was not blocked on a real shell/test process. There was no matching long-running
dotnet test, shell command, or project command still alive on the machine.The last assistant message had completed a normal step with
finish: "tool-calls", then a following assistant message was created with zero token accounting and no useful progress. The final persisted part was abashtool placeholder that never advanced beyondpending:{ "type": "tool", "tool": "bash", "callID": "call_REDACTED", "state": { "status": "pending", "input": {}, "raw": "" } }The adjacent event sequence was effectively:
The important distinction is the same one noted above in this issue: this does not look like a completed model-emitted
{}argument object. It looks like OpenCode persisted the initialtool-input-startplaceholder and never reached the later event that would carry complete executable arguments.Why this looks like the same root family
The stored
bashpart has exactly the intermediate shape discussed earlier:status: "pending"input: {}raw: ""Since the part is still pending, there is no command to execute and no running shell process that can later produce a result. From the user perspective the session looks like it is still working, but provider/API traffic has stopped because the local session/tool-call lifecycle has no recoverable next step.
Source-level correspondence in current
devCurrent
packages/opencode/src/session/processor.tsstill initializes a new tool part inensureToolCall()with:and only moves it to
runningwhen a latertool-callevent provides completed input.The current cleanup code does attempt to convert remaining tool calls to
Tool execution aborted, but this real session on1.17.15still ended with a persisted pending placeholder. That suggests either cleanup did not run, did not reach this part, or the follow-up assistant message created a new pending placeholder after the interrupted cleanup path.Suggested regression coverage
Please consider a processor-level regression that simulates:
tool-calls.bashthat reachestool-input-start/ placeholder creation.tool-callevent arrives.pending + input:{} + raw:""state.Expected result: the placeholder should be converted into an explicit interrupted/error state, or otherwise removed from model-visible recoverable history. It should not remain as waitable work.
InCerryGit commentedon Jul 9, 2026
Additional sanitized evidence from another occurrence in the same stuck-session family.
This one is slightly different from the earlier
bashcase: the affected tool wasskill, and the session eventually self-recovered after a long delay instead of remaining permanently stuck.Environment
1.17.8openai/gpt-5.5Observed timeline
The user requested a read-only comparison task. The session then entered this sequence:
The initially stuck part had this shape:
{ "type": "tool", "tool": "skill", "callID": "call_REDACTED", "state": { "status": "pending", "input": {}, "raw": "" } }After the delay, the same part was converted to an interrupted error:
{ "type": "tool", "tool": "skill", "callID": "call_REDACTED", "state": { "status": "error", "input": {}, "raw": "", "error": "Tool execution aborted", "metadata": { "interrupted": true } } }The next continuation then successfully loaded the same skill with real input:
{ "type": "tool", "tool": "skill", "callID": "call_REDACTED", "state": { "status": "completed", "input": { "name": "REDACTED_SKILL_NAME", "user_message": "REDACTED_TASK_DESCRIPTION" } } }Why this looks related
This reinforces the distinction already discussed above:
input: {}plusraw: ""looks like an initialtool-input-startplaceholder, not a completed model-emitted empty JSON argument object.The new part of this report is the long recovery window:
Tool execution aborted, retried/continued, and completed the task.So the user-visible failure mode is not only "permanently stuck". It can also be "stuck for a long time, then self-recovers after an interrupted pending tool input is aborted".
Suggested regression coverage
Please consider covering a non-
bashtool as well, e.g.skillor another tool-loader path:A tool input starts and creates a pending placeholder:
{ "tool": "skill", "state": { "status": "pending", "input": {}, "raw": "" } }The stream/runtime does not deliver the completed
tool-callevent for several minutes or is interrupted before it arrives.The processor should not leave the UI/session looking active indefinitely.
The pending placeholder should transition quickly to an explicit interrupted/error state, or the user should get a clear retry/error status.
A later continuation should not require waiting for a long timeout/backoff window before progress resumes.
This looks closely related to #33687 and #35454 as well: interrupted tool-input state and UI/session convergence after a tool is durably aborted.
github-actions commentedon Sep 8, 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.