Skip to content

altimate-code run wedges silently on inherited stdin when invoked as subprocess #934

@sahrizvi

Description

@sahrizvi

Problem

When altimate-code run "<task>" --yolo is invoked as a subprocess (Claude Code's Bash tool, Python subprocess.run, a CI runner, a plugin spawn with stdio: "inherit" on stdin), the process hangs at 0% CPU forever. No session is created, no log activity past json migration complete, no error — looks identical to a deadlock.

Root cause

packages/opencode/src/cli/cmd/run.ts:422 unconditionally reads stdin whenever the process is not attached to a TTY:

if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text())

When the parent process inherits stdin from a higher-level non-pipe source and never closes its end, Bun.stdin.text() waits forever for an EOF that never arrives. The intent of the code was "if someone pipes input in, append it to the message" — but the guard !process.stdin.isTTY is too broad: it also matches "stdin was inherited from a parent that isn't a TTY but also isn't closing".

Reproducer

From a shell with a positional message arg:

# Simulate inherited-but-not-closed stdin (Bun.stdin.text() blocks forever)
exec 3< /dev/null  # any open FD that isn't being written to
altimate-code run "say hi" --yolo <&3
# hangs at 0% CPU, no session created

Or, more realistically, from Python subprocess.run([...], stdin=None) inside another process — anywhere the child inherits a stdin file descriptor the parent doesn't explicitly close.

Current mitigation (downstream)

The altimate-opencode-plugin works around this by spawning altimate-code with stdio: ["ignore", "pipe", "pipe"], which closes the child's stdin from outside. See altimate-opencode-plugin plugins/altimate-code/index.ts:28 and its comment at lines 177-182. Any caller that doesn't take that precaution still hits the wedge.

Proposed fix

Only read stdin when no positional message has been provided:

if (!process.stdin.isTTY && message.trim().length === 0) {
  message += "\n" + (await Bun.stdin.text())
}

This matches conventional CLI semantics (echo "foo" | tool reads stdin; tool "explicit arg" doesn't), and unblocks every subprocess caller that passes a positional message — which is the most common pattern in agent/skill/plugin invocations.

Pipe-only invocations without a positional arg (echo "task" | altimate-code run --yolo) continue to work unchanged.

Test plan

  • Unit test: with a positional message provided and isTTY false, Bun.stdin.text() is NOT invoked.
  • Unit test: with no positional message and isTTY false, stdin IS read (existing pipe-only path).
  • Smoke: altimate-code run "say hi" --yolo <&- returns in ~1s instead of hanging.

Context

Surfaced during a multi-week experiment series running altimate-code as a Skill from Claude Code's --print subprocess flow. Originally diagnosed and patched in a local altimate-code-preview build (worktree at altimate-code/.claude/worktrees/fix-run-stdin/) — that patch has been belt-and-suspenders for the last several months. Re-verified 2026-06-12 against main (release v0.8.7, commit 146acea8e): run.ts:422 is unchanged; the line is still the original unconditional read.

Full writeup: plugin-skill-experiments/03-issues-and-fixes.md Issue #2.

PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions