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.
Problem
When
altimate-code run "<task>" --yolois invoked as a subprocess (Claude Code's Bash tool, Pythonsubprocess.run, a CI runner, a plugin spawn withstdio: "inherit"on stdin), the process hangs at 0% CPU forever. No session is created, no log activity pastjson migration complete, no error — looks identical to a deadlock.Root cause
packages/opencode/src/cli/cmd/run.ts:422unconditionally reads stdin whenever the process is not attached to a TTY: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.isTTYis 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:
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-pluginworks around this by spawning altimate-code withstdio: ["ignore", "pipe", "pipe"], which closes the child's stdin from outside. See altimate-opencode-pluginplugins/altimate-code/index.ts:28and 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:
This matches conventional CLI semantics (
echo "foo" | toolreads 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
isTTYfalse,Bun.stdin.text()is NOT invoked.isTTYfalse, stdin IS read (existing pipe-only path).altimate-code run "say hi" --yolo <&-returns in ~1s instead of hanging.Context
Surfaced during a multi-week experiment series running
altimate-codeas a Skill from Claude Code's--printsubprocess flow. Originally diagnosed and patched in a localaltimate-code-previewbuild (worktree ataltimate-code/.claude/worktrees/fix-run-stdin/) — that patch has been belt-and-suspenders for the last several months. Re-verified 2026-06-12 againstmain(releasev0.8.7, commit146acea8e):run.ts:422is unchanged; the line is still the original unconditional read.Full writeup:
plugin-skill-experiments/03-issues-and-fixes.mdIssue #2.PR incoming.