anomalyco / anomalyco/opencode
Shell tool never returns when a command leaves a background process holding its stdio
@nexxeln is already working on this.
Since Sep 4, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
The shell tool appears to treat a command as finished when its stdout/stderr
reach EOF, rather than when the spawned child exits.
Any command that leaves a
process running after the direct child exits therefore hangs the tool forever:
the child has exited, but the descriptors it passed on are still open, so EOF
never arrives.
The agent generated an ordinary Vite + Node dev launcher and then hung itself on it 3x in one
session.
It appears there is no timeout. The last occurrence sat for 1 hour 44 minutes with no
recovery path, and the session could only be freed by finding and killing the
orphaned processes from another terminal. The UI showed the turn as QUEUED
throughout, which I interpreted as "the model is slow" rather than "this tool call will
never return" — I spent a long time investigating my model provider before
finding the real cause.
What the agent ran
It generated this launcher, which spawns two long-lived children and then
exits:
// scripts/dev.mjs
import { spawn } from 'node:child_process';
const run = (cmd, args, opts = {}) =>
spawn(cmd, args, { cwd: root, stdio: 'inherit', shell: false, ...opts });
const server = run('node', ['server/server.js']);
const client = run('npx', ['vite']);
function shutdown() { server.kill(); client.kill(); process.exit(0); }
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
node scripts/dev.mjs exits immediately. Both children keep running.
State after the hang
Both children are alive, reparented to init, and still holding the tool's
descriptors:
pid 47588 node server/server.js PPID 1
fd 1 -> unix socket 0xba9ae00d...
fd 2 -> unix socket 0xadc3d307...
pid 47601 vite PPID 1
fd 1 -> /dev/null
fd 2 -> unix socket 0xadc3d307... <- same socket as pid 47588
Two details that cost me time and may save you some:
- Both processes share the same stderr socket. Killing only one does not
release the tool call; both have to go. vitealready had stdout on/dev/null, and stderr alone was enough to
keep the tool blocked. A partial redirect is not a workaround.
Timeline of the third occurrence
17:02:11 previous turn completes normally
17:02:12 agent runs the dev script; both children orphan to PPID 1
18:46 tool call still waiting
My model provider received 0 requests in that window, so the request was
never sent; the stall is entirely on the tool call and not on generation (for example).
2 things
- Completion appears to be keyed on stream EOF rather than child exit. The direct
child exited immediately at 17:02:12; only the inherited descriptors kept
the stream open. - The wait seems to have no timeout. Even if (1) is intentional, a tool call that can
block indefinitely with no timeout and no cancellation leaves the session
unrecoverable from inside OpenCode.
I'd argue (2) is more serious: what might otherwise be a recoverable annoyance
becomes a dead session.
Suggested fixes
- Resolve the tool call on child exit and treat later stream output as
best-effort; or give the child its own stdio rather than passing a live pipe
through to grandchildren. - Add a timeout, and on firing name the processes still holding the descriptors
so the cause is visible withoutlsof. - Distinguish "waiting on tool output" from "waiting on the model" in the UI.
The current queued display sent me looking in the wrong place for over an
hour.
On the generated code
stdio: 'inherit' is idiomatic and correct for a dev launcher run in a
terminal: it is how to see server logs. I think issue is that a standard npm run dev can wedge the
agent permanently.
Plugins
No response
OpenCode version
1.18.20
Steps to reproduce
-
Create a dev script that spawns a long-lived child with inherited stdio and
then exits:// scripts/dev.mjs import { spawn } from 'node:child_process'; spawn('node', ['server.js'], { stdio: 'inherit' }); -
Ask the agent to run it (
node scripts/dev.mjs, or annpm run devwired
to the same). -
node scripts/dev.mjsexits, but the spawned server keeps running and keeps
the inherited stdout/stderr open. -
The tool call never returns. The session shows as queued indefinitely and no
further requests reach the model provider. -
Confirm:
ps -o ppid= -p <server pid>reports 1, andlsof -p <server pid>
shows fd 1/2 as unix sockets. -
Killing the spawned processes releases the tool call.
Reproduced multiple in one session with a Vite + Node dev launcher. The
minimal script in step 1 is derived from the observed mechanism rather than
separately exercised.
### Screenshot and/or share link
_No response_
### Operating System
macOS 26.4 (build 25E246), Apple silicon
### Terminal
iTerm2 3.6.10
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.