Skip to content

[FEATURE]: let opencode run ignore an unused inherited stdin pipe #42064

Closed

Description

@vladislav-miroshnikov
  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

opencode run reads non-TTY stdin to EOF before starting a prompt. That is the right default for intentional input such as:

cat context.txt | opencode run "review this"

The problem is that some supervisors give every child a pipe and keep its writer open, even when the command has no stdin payload. The prompt then never starts. This minimal Python control retains the pipe deliberately; changing PIPE to DEVNULL makes the same command progress to the expected invalid-model error:

import subprocess

args = ["opencode", "run", "--model", "invalid/provider-model", "reply with OK"]

def run(stdin):
    child = subprocess.Popen(args, stdin=stdin)
    try:
        return f"exit {child.wait(timeout=5)}"
    except subprocess.TimeoutExpired:
        child.terminate()
        child.wait()
        return "still waiting after 5s"

print("open pipe:", run(subprocess.PIPE))
print("closed input:", run(subprocess.DEVNULL))

Could run expose an explicit opt-out, for example --no-stdin? It would skip only the stdin read for that invocation; the existing default and pipe + positional prompt behavior would not change. I have a small implementation and subprocess tests ready if this interface is acceptable.

The symptom also appeared in #11891, which documented DEVNULL as a caller-side workaround. Related #25508 and merged #16300 show why silently ignoring piped input is not safe. When the caller controls descriptors, DEVNULL, stdin: "ignore", or closing the writer remains the workaround.

Activity

  1. xichen1997 commented on Aug 13, 2026

    @xichen1997

    I have draft a fix PR: #42235

  2. postoso commented on Aug 21, 2026

    @postoso

    I hit the same hang from a wrapper that leaves the child's stdin pipe open. #38723 now has a deterministic FIFO repro at run.ts:416, and the original reporter reproduced it and retracted the models.dev diagnosis.

    I tested the alternative where a supplied positional message makes stdin ignored. It fixes the hang, but it also silently drops input from opencode run "prompt" < file, including the empty-positional cases. That mixed-input behavior was added explicitly, so I think --no-stdin is the safer interface.

    I have subprocess tests that leave stdin open and fail by timeout on the current code, including empty positional and -- "" cases.

  3. teoxis commented on Oct 19, 2026

    @teoxis

    I hit this too: I run opencode run "<prompt>" from a nightly orchestrator script, and the child inherits a stdin pipe that the orchestrator never closes. Every run hung forever with no output; </dev/null fixed it, which is how I traced it back here.

    Still reproducible on v1.19.3 and on current main. Minimal repro (isolated XDG dirs, nonexistent model, so a healthy run fails in under a second):

    import os, subprocess
    cmd = ["opencode", "run", "--pure", "--model", "nonexistent/model", "hello"]
    subprocess.run(cmd, stdin=subprocess.DEVNULL, timeout=25)   # exits in ~0.7s
    r, w = os.pipe()                                             # write end kept open, never written
    subprocess.run(cmd, stdin=r, timeout=25)                     # TimeoutExpired: hangs

    Cause: stdin is read to EOF whenever it is not a TTY, even when the message is passed as an argument:

    Proposed fix: I agree with @postoso that an opt-out flag is the safer interface, since ignoring stdin whenever a positional message exists would silently drop opencode run "prompt" < file. #44557 implemented --no-stdin but was closed by the stale-PR cleanup without review, and it targeted the old packages/opencode CLI, which no longer exists on main.

    I have the same approach ported to the new CLI on main (3 files, +8/−1):

    • a --no-stdin boolean flag on run (default false, so the current behavior is unchanged);
    • run.ts: const piped = input.noStdin || process.stdin.isTTY ? undefined : await readStdin().

    It also adds subprocess tests against a minimal fake server: --no-stdin "hello" returns with an open, never-closing stdin; with --no-stdin, piped input is not used as the message; and piped input is still read by default. tsc -b and the existing test/run suite pass.

    Happy to open the PR if a maintainer is OK with the --no-stdin interface. If it should be named or shaped differently (e.g. --stdin=false, or an env var for wrappers), I can adjust.

  4. rubenxyz commented on Oct 25, 2026

    @rubenxyz

    Confirmed unchanged on 2.1.4 (Linux x64, 2026-10-25), from a wrapper that leaves the child's stdin pipe open:

    • opencode run --model <model> "reply with the single word OK" with an inherited, still-open stdin pipe: no output, still running when timeout 45 killed it (rc 124).
    • the identical command with </dev/null: finished in 3.7 s with OK.

    So the supervisor/inherited-pipe case in this report still reproduces on 2.x main. The workaround wrappers can apply today: close the child's stdin (</dev/null, stdin=subprocess.DEVNULL) and keep a timeout around the call.

  5. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the clear write-up and the minimal reproduction.

    To restate: you're asking for an explicit opt-out on opencode run, e.g. --no-stdin, that skips reading non-TTY stdin for that call. That way an inherited pipe whose writer is never closed doesn't block the prompt, and the default and pipe + positional prompt behaviour stay as they are. The current code still reads stdin to EOF whenever it isn't a TTY (packages/cli/src/run/run.ts), so this would be a new option rather than a bug fix. Maintainers will consider it.

    Related issues and PRs:

    Until then, stdin=subprocess.DEVNULL, stdin: "ignore" or < /dev/null is still the workaround when you control the child's descriptors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions