anomalyco / anomalyco/opencode

Shell tool never returns when a command leaves a background process holding its stdio

Open
#47,350 4 comments 0 reactions 1 assignee View on GitHub

@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.
  • vite already 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
  1. 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.
  2. 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 without lsof.
  • 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
  1. 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' });
    
  2. Ask the agent to run it (node scripts/dev.mjs, or an npm run dev wired
    to the same).

  3. node scripts/dev.mjs exits, but the spawned server keeps running and keeps
    the inherited stdout/stderr open.

  4. The tool call never returns. The session shows as queued indefinitely and no
    further requests reach the model provider.

  5. Confirm: ps -o ppid= -p <server pid> reports 1, and lsof -p <server pid>
    shows fd 1/2 as unix sockets.

  6. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.