Description
Bug: Bash tool hangs indefinitely when PowerShell uses Start-Process with -RedirectStandardOutput/-RedirectStandardError on Windows
Summary
When the opencode bash tool executes a PowerShell command that combines:
Start-Process with -RedirectStandardOutput and/or -RedirectStandardError
- A long-running child process (e.g.
dotnet run, npm run dev)
- Followed by synchronous commands (e.g.
; Start-Sleep ...; netstat ...)
…then the spawned child process starts successfully (e.g. web server listens on its port), but the opencode bash tool never returns. It stays in status: "running" forever, blocking the entire session.
Environment
- OS: Windows 11 (win32, x64)
- Shell: PowerShell 5.1 (via opencode
bash tool)
- opencode version: installed globally via npm —
C:\Users\thailc\AppData\Roaming\npm\node_modules\opencode-ai\bin\opencode.exe
- Model provider: zhipuai-coding-plan / glm-5.2
- Project: .NET 8 solution (
dotnet run --launch-profile CuDanAPI)
Reproduction
-
Start any opencode session on Windows.
-
Ask the agent (or paste yourself) the following PowerShell command via the bash tool:
Start-Process -FilePath "dotnet" -ArgumentList "run","--no-build","--launch-profile","CuDanAPI" `
-WorkingDirectory "C:\path\to\project" `
-WindowStyle Hidden `
-RedirectStandardOutput "C:\Temp\app.log" `
-RedirectStandardError "C:\Temp\app.err";
Start-Sleep -Seconds 8;
netstat -ano | findstr ":5000.*LISTENING"
-
Observe:
- The child
dotnet process starts and binds port 5000 correctly (verifiable via Task Manager / netstat from another shell).
- The redirect files (
app.log, app.err) are written to and contain the expected "Now listening on: http://localhost:5000" line.
- But the
bash tool call never completes. The spinner keeps spinning and the prompt never comes back.
Expected behavior
The tool call should return once the synchronous part of the command (Start-Sleep + netstat) finishes — i.e. within ~8 seconds. The Start-Process-spawned child is detached and should not keep the parent pipe open.
Actual behavior
The tool is stuck in running state indefinitely. Inspecting the SQLite database at %USERPROFILE%\.local\share\opencode\opencode.db:
-- The tool part has a start time but NO end time:
SELECT json_extract(data, '$.state.status') AS status,
json_extract(data, '$.state.time.start') AS t_start,
json_extract(data, '$.state.time.end') AS t_end
FROM part
WHERE id = 'prt_f7d43da3e001ZUAb0xC1MJHgbA';
-- status = "running"
-- t_start = 1784513034969
-- t_end = NULL
Meanwhile, on the OS:
> Get-Process CuDanAPI
Id ProcessName StartTime CPU
-- ----------- --------- ---
24324 CuDanAPI 7/20/2026 9:03:47 AM 0.65625
> netstat -ano | findstr ":5000.*LISTENING"
TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 24324
TCP [::1]:5000 [::]:0 LISTENING 24324
So the child is alive and well — only the tool wrapper is deadlocked.
Root cause (hypothesis)
When Start-Process is invoked with -RedirectStandardOutput / -RedirectStandardError, PowerShell opens pipe handles to capture the child's stdout/stderr. Even after Start-Process returns and the synchronous part of the command finishes, the child process still holds the inherited write end of those pipes. PowerShell's own stdout (which opencode reads from) inherits / shares the same pipe, so opencode never receives EOF and keeps waiting for more output.
This is a known class of bug on Windows (see similar reports in Node.js, Git Bash, and other shells that wrap child processes via pipes). The two contributing factors:
-WindowStyle Hidden keeps the child alive after the parent returns.
-RedirectStandardOutput/Error creates a pipe whose write handle is inherited by the child.
Either factor alone is usually fine; the combination is lethal.
Workaround (already documented in our AGENTS.md)
Rule 1 — Never redirect in Start-Process when called from an opencode tool.
If you must spawn a background server:
# Tool call #1 — spawn detached, NO redirection, NO trailing synchronous commands
Start-Process -FilePath "dotnet" -ArgumentList "run","--no-build","--launch-profile","CuDanAPI" `
-WorkingDirectory "C:\path\to\project"
# Tool call #2 — wait, then check (separate call)
Start-Sleep -Seconds 6
netstat -ano | findstr ":5000.*LISTENING"
If logging is needed, write logs inside the child itself, or use Start-Job + Receive-Job.
Suggested fix
A few possible directions, in order of preference:
- Per-call timeout on the
bash tool. Allow the user (or agent) to specify a hard timeout after which opencode forcibly closes the pipe and marks the tool as error: timeout. The current timeout parameter only seems to apply while the command is actively writing; if the pipe is held open but silent, it never fires.
- EOF detection heuristic. If a PowerShell command finishes (child process exits) but the pipe is still held by a grandchild process, opencode could detect the parent shell has exited and close the tool.
- Documentation / guardrail. At minimum, document this pattern prominently under "Windows quirks" in the official docs, and/or have the agent-facing system prompt warn against
Start-Process -RedirectStandard* + -WindowStyle Hidden.
- Optional: bypass the issue by running PowerShell with
--NoProfile -Command and explicitly closing inherited handles via Set-Process Mitigation or similar. (More invasive.)
Impact
This is a silent, easy-to-trigger deadlock that looks like a hung session. The agent has no way to recover (it can only wait). On a long coding session this means losing all in-memory context unless the user knows to kill the opencode.exe process and opencode resume the session.
The bug has bitten us multiple times in our team workflow (we maintain a Building Management Information System on .NET 8 and routinely need to spawn dotnet run for CuDanAPI during development).
Logs / diagnostics
- opencode version: latest from npm at time of writing (July 2026)
- DB:
%USERPROFILE%\.local\share\opencode\opencode.db, table part, columns data.state.time.start / data.state.time.end
- Sample tool part JSON showing the stuck state:
{
"type": "tool",
"tool": "bash",
"state": {
"status": "running",
"input": {
"command": "Start-Process -FilePath \"dotnet\" ... -RedirectStandardOutput \"...\" -RedirectStandardError \"...\"; Start-Sleep -Seconds 8; netstat -ano | findstr \":5000.*LISTENING\"",
"workdir": "C:\\Users\\...\\BMIS"
},
"time": { "start": 1784513034969 },
"metadata": { "output": "TCP 127.0.0.1:5000 ... LISTENING 24324" }
}
}
Note: the metadata.output already contains the expected netstat result — proving the synchronous part of the command ran — yet status is still "running" and time.end is missing.
Related
- Similar reports in other tools: Node.js
child_process.spawn with inherited stdio on Windows; VS Code's integrated terminal; Git Bash's handling of start /B.
Happy to provide more diagnostics or test a fix if needed. Thanks for the great tool! 🙏
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Description
Bug: Bash tool hangs indefinitely when PowerShell uses
Start-Processwith-RedirectStandardOutput/-RedirectStandardErroron WindowsSummary
When the opencode
bashtool executes a PowerShell command that combines:Start-Processwith-RedirectStandardOutputand/or-RedirectStandardErrordotnet run,npm run dev); Start-Sleep ...; netstat ...)…then the spawned child process starts successfully (e.g. web server listens on its port), but the opencode
bashtool never returns. It stays instatus: "running"forever, blocking the entire session.Environment
bashtool)C:\Users\thailc\AppData\Roaming\npm\node_modules\opencode-ai\bin\opencode.exedotnet run --launch-profile CuDanAPI)Reproduction
Start any opencode session on Windows.
Ask the agent (or paste yourself) the following PowerShell command via the
bashtool:Observe:
dotnetprocess starts and binds port 5000 correctly (verifiable via Task Manager /netstatfrom another shell).app.log,app.err) are written to and contain the expected "Now listening on: http://localhost:5000" line.bashtool call never completes. The spinner keeps spinning and the prompt never comes back.Expected behavior
The tool call should return once the synchronous part of the command (
Start-Sleep+netstat) finishes — i.e. within ~8 seconds. TheStart-Process-spawned child is detached and should not keep the parent pipe open.Actual behavior
The tool is stuck in
runningstate indefinitely. Inspecting the SQLite database at%USERPROFILE%\.local\share\opencode\opencode.db:Meanwhile, on the OS:
So the child is alive and well — only the tool wrapper is deadlocked.
Root cause (hypothesis)
When
Start-Processis invoked with-RedirectStandardOutput/-RedirectStandardError, PowerShell opens pipe handles to capture the child's stdout/stderr. Even afterStart-Processreturns and the synchronous part of the command finishes, the child process still holds the inherited write end of those pipes. PowerShell's own stdout (which opencode reads from) inherits / shares the same pipe, so opencode never receives EOF and keeps waiting for more output.This is a known class of bug on Windows (see similar reports in Node.js, Git Bash, and other shells that wrap child processes via pipes). The two contributing factors:
-WindowStyle Hiddenkeeps the child alive after the parent returns.-RedirectStandardOutput/Errorcreates a pipe whose write handle is inherited by the child.Either factor alone is usually fine; the combination is lethal.
Workaround (already documented in our AGENTS.md)
Rule 1 — Never redirect in
Start-Processwhen called from an opencode tool.If you must spawn a background server:
If logging is needed, write logs inside the child itself, or use
Start-Job+Receive-Job.Suggested fix
A few possible directions, in order of preference:
bashtool. Allow the user (or agent) to specify a hard timeout after which opencode forcibly closes the pipe and marks the tool aserror: timeout. The currenttimeoutparameter only seems to apply while the command is actively writing; if the pipe is held open but silent, it never fires.Start-Process -RedirectStandard*+-WindowStyle Hidden.--NoProfile -Commandand explicitly closing inherited handles viaSet-Process Mitigationor similar. (More invasive.)Impact
This is a silent, easy-to-trigger deadlock that looks like a hung session. The agent has no way to recover (it can only wait). On a long coding session this means losing all in-memory context unless the user knows to kill the
opencode.exeprocess andopencode resumethe session.The bug has bitten us multiple times in our team workflow (we maintain a Building Management Information System on .NET 8 and routinely need to spawn
dotnet runfor CuDanAPI during development).Logs / diagnostics
%USERPROFILE%\.local\share\opencode\opencode.db, tablepart, columnsdata.state.time.start/data.state.time.end{ "type": "tool", "tool": "bash", "state": { "status": "running", "input": { "command": "Start-Process -FilePath \"dotnet\" ... -RedirectStandardOutput \"...\" -RedirectStandardError \"...\"; Start-Sleep -Seconds 8; netstat -ano | findstr \":5000.*LISTENING\"", "workdir": "C:\\Users\\...\\BMIS" }, "time": { "start": 1784513034969 }, "metadata": { "output": "TCP 127.0.0.1:5000 ... LISTENING 24324" } } }Note: the
metadata.outputalready contains the expectednetstatresult — proving the synchronous part of the command ran — yetstatusis still"running"andtime.endis missing.Related
child_process.spawnwith inherited stdio on Windows; VS Code's integrated terminal; Git Bash's handling ofstart /B.Happy to provide more diagnostics or test a fix if needed. Thanks for the great tool! 🙏
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response