Skip to content

fix: skip stdin read when positional message provided#935

Open
sahrizvi wants to merge 1 commit into
mainfrom
fix/run-stdin-wedge-934
Open

fix: skip stdin read when positional message provided#935
sahrizvi wants to merge 1 commit into
mainfrom
fix/run-stdin-wedge-934

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Closes #934

Summary

altimate-code run "<task>" hangs at 0% CPU forever when invoked as a subprocess from a context that inherits stdin without closing it (Claude Code's Bash tool, Python subprocess.run, CI, plugin hosts that don't pin stdin to /dev/null). The cause is an over-broad guard at packages/opencode/src/cli/cmd/run.ts:433:

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

!process.stdin.isTTY matches not just "someone piped input in" (the intended case) but also "stdin was inherited from a non-TTY parent that hasn't closed it" (the wedge case). In the second case Bun.stdin.text() waits forever for an EOF that never arrives.

Fix

Only read stdin when no positional message was provided:

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

This matches conventional CLI semantics (positional argument overrides stdin — tool "explicit arg" doesn't try to consume stdin; echo "task" | tool still does) and unblocks every subprocess caller that passes a positional message, which is the dominant pattern in agent / skill / plugin invocations of altimate-code run.

Downstream context

Downstream consumers have been working around the wedge by spawning altimate-code with stdio: ["ignore", "pipe", "pipe"] — see altimate-opencode-plugin plugins/altimate-code/index.ts:28 and its lineage comment at lines 177-182 explicitly referencing this bug. That workaround is fine for the plugin itself but doesn't protect any other caller (Claude Code's Bash tool, ad-hoc CI scripts, future plugins that forget). This PR is the proper fix at the source.

Test plan

  • Local smoke: altimate-code-preview run "say hi" --yolo (built from a worktree with the same patch) returns in ~1.3s instead of hanging — has been running this way for ~9 days against real workloads.
  • Reviewer: run altimate-code run "say hi and exit" --yolo from a Python subprocess.run(..., stdin=None) parent — expect completion in seconds, not a hang.
  • Reviewer: pipe-only path unchanged: echo "hello task" | altimate-code run --yolo still reads stdin (no positional arg → guard's second condition message.trim().length === 0 is true → stdin is consumed).

No new unit test added — there's no existing test for cmd/run.ts, and the function is a CLI entry point that's awkward to mock at this granularity (needs spies on process.stdin.isTTY and Bun.stdin.text() plus the surrounding setup). Happy to add one if reviewers prefer; the conditional itself is small enough that the diff is largely self-documenting.

Risk

Very low. The change tightens an existing guard — any caller that should be reading stdin (no positional message) still does; the only behavior change is for callers that both pass a positional message and inherit non-TTY stdin, which is exactly the population that has been silently hanging.

Links


Summary by cubic

Prevented altimate-code run from hanging by only reading stdin when no positional message is provided. Subprocess calls with inherited non‑TTY stdin now complete as expected while pipe-based input still works.

  • Bug Fixes
    • Read from stdin only when message is empty, updating guard in packages/opencode/src/cli/cmd/run.ts.
    • Avoids indefinite wait on Bun.stdin.text() when called from tools like Python subprocess.run, CI, or plugin hosts.
    • Pipe-only usage unchanged: echo "task" | altimate-code run still reads stdin; altimate-code run "task" skips stdin.

Written for commit a387257. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where the command would hang indefinitely when stdin was inherited but never closed, even when no interactive input was required. The command now intelligently manages stdin input, only reading when necessary, preventing unnecessary process hangs in automated or piped environments.

`!process.stdin.isTTY` alone is too broad — it matches not just
"someone piped input in" but also "the parent process inherited
stdin from somewhere upstream and never closed its end." In the
second case, `Bun.stdin.text()` waits forever for an EOF that
never arrives. The process sits at 0% CPU with no session
created, no log activity, no error — a silent hang.

This surfaces in every subprocess invocation pattern that passes
a positional message and inherits stdin: Claude Code's Bash tool,
Python `subprocess.run(..., stdin=None)`, CI runners, plugin
hosts that don't pin stdin to `/dev/null`. Downstream callers
have been working around it with `stdio: "ignore"` on spawn
(see e.g. altimate-opencode-plugin
`plugins/altimate-code/index.ts:28`), but anything that forgets
to do that still hits the wedge.

Fix: only read stdin when no positional `message` was provided.
Matches conventional CLI semantics (positional overrides stdin)
and unblocks every subprocess caller. Pipe-only invocations
without a positional arg continue to work unchanged.

Closes #934
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a2c1b145-84ea-40a0-a540-f20cc6dfc800

📥 Commits

Reviewing files that changed from the base of the PR and between 146acea and a387257.

📒 Files selected for processing (1)
  • packages/opencode/src/cli/cmd/run.ts

📝 Walkthrough

Walkthrough

The run command's stdin handling is now conditional: it reads from stdin only when the process is non-TTY and no positional message was provided, preventing hangs on inherited stdin file descriptors that never close. Added comments explain the guard behavior.

Changes

Conditional stdin read guard

Layer / File(s) Summary
Conditional stdin read guard
packages/opencode/src/cli/cmd/run.ts
stdin is read from Bun.stdin.text() only when stdin is non-TTY and the assembled message is empty (message.trim().length === 0). Previously, stdin was read unconditionally on non-TTY, causing indefinite waits for EOF on inherited but unclosed file descriptors. Pipe-only invocations without a positional arg continue to work unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit once hung on inherited streams,
Waiting for EOF in restless dreams.
"Check the message first!" the bunny did say,
And stdin hangs ceased to ruin the day. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is comprehensive and well-structured, but it does not include the required 'PINEAPPLE' marker at the top that the template mandates for AI-generated contributions. Add 'PINEAPPLE' at the very top of the PR description as required by the template for AI-generated contributions.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: conditionally skipping stdin read when a positional message is provided, which is the core fix for the hanging behavior.
Linked Issues check ✅ Passed The code changes directly address issue #934's objectives: the stdin read is now conditional on both non-TTY status AND empty positional message, preventing hangs for subprocess invocations while preserving pipe-only behavior.
Out of Scope Changes check ✅ Passed The change is narrowly scoped to the specific stdin-reading guard in run.ts, directly addressing the hang issue without introducing unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/run-stdin-wedge-934

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

❌ Tests — Failures Detected

TypeScript — 15 failure(s)

  • connection_refused [1.00ms]
  • timeout
  • permission_denied
  • parse_error
  • oom [1.00ms]
  • network_error
  • auth_failure
  • rate_limit
  • internal_error
  • empty_error
  • connection_refused
  • timeout [1.00ms]
  • permission_denied
  • parse_error
  • network_error

Next Step

Please address the failing cases above and re-run verification.

cc @sahrizvi

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants