Skip to content

fix: Claude Code TUI falls back to non-interactive mode in cmux (no PTY allocated) (#4001) - #4002

Closed
moaedy wants to merge 3 commits into
manaflow-ai:mainfrom
moaedy:godfix/issue-4001-run-13
Closed

moaedy wants to merge 3 commits into
manaflow-ai:mainfrom
moaedy:godfix/issue-4001-run-13

Conversation

@moaedy

@moaedy moaedy commented May 12, 2026 •

Copy link
Copy Markdown

# Summary

  • Wrap exec "$REAL_CLAUDE" calls in Resources/bin/claude with /usr/bin/script -q /dev/null via a new exec_real_claude helper, so the Ink-based TUI sees a PTY on stdin.
  • Without this, cmux build variants that route child IO without a controlling terminal made Claude Code fall back to non-interactive mode. ## Testing
  • Updated tests/test_claude_wrapper_hooks.py to cover the new exec path.
  • Verified the helper probes /usr/bin/script first so a broken stdin can't regress claude into a "failed to start" state. ## Review Trigger (Copy/Paste as PR comment) ```text
    @codex review
    @coderabbitai review
    @greptile-apps review
    @cubic-dev-ai review
- [x] I tested the change locally
- [x] I added or updated tests for behavior changes
- [ ] I updated docs/changelog if needed
- [ ] I requested bot reviews after my latest commit (copy/paste block above or equivalent)
- [ ] All code review bot comments are resolved
- [ ] All human review comments are resolved Fixes #4001

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Keep Claude Code’s TUI interactive under `cmux` by allocating a PTY when stdin isn’t a terminal. Adds a safe `/usr/bin/script` exec path with probing to prevent non-interactive fallback (fixes #4001).

- **Bug Fixes**
  - Added `exec_real_claude` in `Resources/bin/claude` and used it for all real `claude` execs (pass-through, hooks, session-id) to wrap with `/usr/bin/script -q /dev/null` when `IN_CMUX=1`, stdin is not a TTY, and `CMUX_CLAUDE_FORCE_PTY` != `0` (covers `claude` and `claude agents`).
  - Probes `/usr/bin/script` with `/usr/bin/true` and falls back to direct exec if unavailable or unsafe to avoid start failures.
  - Extended tests with a PTY allocation harness that asserts the real `claude` sees a TTY for both entry and `agents`, and verifies `CMUX_CLAUDE_FORCE_PTY=0` disables PTY allocation; skips when `/usr/bin/script` is missing.

<sup>Written for commit 61e8d0d58ed535aa48d07762c64071c4a46836c2. Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Improvements**
  * Ensures claude is launched with a proper terminal (PTY) across environments and build variants, improving interactive behavior, hook handling, and session handling; adds an opt-out for PTY allocation.

* **Tests**
  * Adds regression tests verifying PTY allocation, successful terminal detection for real claude, and opt-out behavior across launch modes and CMUX socket states.

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/manaflow-ai/cmux/pull/4002)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

@lebara-moaead is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 12, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds exec_real_claude that conditionally allocates a PTY via BSD script, routes multiple claude exec paths through it, and adds tests that verify the spawned real claude observes a TTY when run inside cmux without a TTY on stdin.

Changes

PTY Wrapper and Integration

Layer / File(s) Summary
exec_real_claude wrapper function
Resources/bin/claude
New function conditionally allocates a PTY via BSD script when running in certain cmux/socket states, otherwise execs claude directly.
Wrapper integration across execution paths
Resources/bin/claude
Replaced direct exec calls in non-CMUX, hooks-injection, and session-id branches to invoke claude through exec_real_claude.
PTY allocation regression tests
tests/test_claude_wrapper_hooks.py
Adds run_wrapper_pty_probe() helper, test_inside_cmux_without_tty_allocates_pty_for_real_claude(), and test_force_pty_opt_out_skips_pty_allocation(); wires tests into main test suite.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I tunneled through sockets, quiet and spry,
Poked a BSD script and gave PTYs a try,
Spawned a real Claude with stdin all bright,
TUIs woke up and danced in the night,
Hooray — no more "not a tty" sigh!

🚥 Pre-merge checks | ✅ 14 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing Claude Code TUI interactive mode in cmux by PTY allocation, which matches the core objective of wrapping exec calls with script to provide PTY.
Description check ✅ Passed The PR description covers the key changes (exec_real_claude wrapper, /usr/bin/script usage), testing approach, and includes most checklist items. While docs/changelog update is unchecked, the description is substantially complete.
Linked Issues check ✅ Passed The PR successfully addresses issue #4001 by implementing PTY allocation via /usr/bin/script wrapper when IN_CMUX=1 and stdin lacks a TTY, with probing for safety and comprehensive tests covering both entry paths and force_pty opt-out.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #4001: exec_real_claude wrapper implementation, PTY allocation logic, safety probing, and test additions with PTY harness are all necessary for the fix.
Cmux Swift Actor Isolation ✅ Passed No Swift files modified in PR. Custom check for Swift 6 actor isolation is not applicable to Bash shell script and Python test changes.
Cmux Swift Blocking Runtime ✅ Passed PR contains no Swift code (only bash shell script and Python tests). Custom check for Swift blocking runtime is not applicable to non-Swift changes.
Cmux No Hacky Sleeps ✅ Passed PR introduces exec_real_claude() to allocate PTYs using /usr/bin/script as a functional fix, not a timing workaround. Tests use deterministic scaffolding. No sleep, delay, or polling patterns found.
Cmux Swift Concurrency ✅ Passed PR changes are bash script (Resources/bin/claude) and Python tests (tests/test_claude_wrapper_hooks.py). Custom check applies only to cmux-owned Swift code; no Swift files modified in this PR.
Cmux Swift @Concurrent ✅ Passed All 484 Swift files in this commit are newly added (status A), not modified. The @concurrent check applies to Swift modifications, not new files.
Cmux Swift File And Package Boundaries ✅ Passed PR modifies only shell script (Resources/bin/claude) and Python tests. No Swift files changed, so the Swift file/package boundary check is not applicable.
Cmux Swift Logging ✅ Passed No Swift files modified. Changes are to a Bash script wrapper and Python test file. Swift logging rule only applies to production Swift code changes.
Cmux Swiftui State Layout ✅ Passed Check not applicable. PR modifies only shell script (Resources/bin/claude) and Python test file (tests/test_claude_wrapper_hooks.py). No SwiftUI code changes present.
Cmux Architecture Rethink ✅ Passed This PR modifies only bash and Python test code. The custom check targets Swift architectural changes per swift-architectural-rethink.md. No Swift files were modified.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies only bash shell script and Python test files; no Swift changes. Custom check only applies to Swift auxiliary window code.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@greptile-apps

greptile-apps Bot commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes Claude Code TUI falling back to non-interactive mode inside cmux by introducing an exec_real_claude helper that wraps every exec \"$REAL_CLAUDE\" call with /usr/bin/script -q /dev/null when IN_CMUX=1, stdin is not a TTY, and CMUX_CLAUDE_FORCE_PTY is not 0. A probe (script -q /dev/null /usr/bin/true) guards against hostile stdin types that make BSD script abort, falling back to direct exec if the probe fails.

  • Added exec_real_claude in Resources/bin/claude and replaced all four exec \"$REAL_CLAUDE\" call-sites (pass-through, hook-bypass, session-id, and skip-session-id paths) to use it.
  • Added test_inside_cmux_without_tty_allocates_pty_for_real_claude (positive PTY test) and test_force_pty_opt_out_skips_pty_allocation (opt-out regression) in tests/test_claude_wrapper_hooks.py; the positive test skips when BSD script is unavailable.

Confidence Score: 4/5

Safe to merge for the interactive TUI fix, but hooks that use CMUX_CLAUDE_PID for signal delivery or liveness checks will silently target the interposed script process rather than the real claude process.

The exec_real_claude helper correctly falls back to direct exec when the BSD-script probe fails, so the change cannot regress non-PTY environments. However, CMUX_CLAUDE_PID=$$ is set with the explicit comment that 'this PID becomes the actual claude process PID,' which held under the old direct exec but no longer holds: exec'ing script makes $$ track the script wrapper, while claude runs as a forked child at a different PID. Any hook that sends signals, polls /proc, or checks process names via CMUX_CLAUDE_PID will silently target the wrong process after this change.

Resources/bin/claude — the CMUX_CLAUDE_PID export and its comment need revisiting now that script is interposed between the wrapper and real claude.

Important Files Changed

Filename Overview
Resources/bin/claude Introduces exec_real_claude helper with PTY allocation via BSD script; CMUX_CLAUDE_PID=$$ (set just before all exec_real_claude call-sites on the hook-injection path) now tracks the script process rather than claude, breaking any hook that uses this PID for signal delivery or liveness checks.
tests/test_claude_wrapper_hooks.py Adds run_wrapper_pty_probe harness and two new tests; positive PTY test correctly skips when BSD script is absent, but opt-out test lacks the same guard so it passes vacuously on non-macOS CI.

Sequence Diagram

sequenceDiagram
    participant cmux as cmux (no PTY)
    participant wrapper as Resources/bin/claude (wrapper, PID=$$)
    participant script as /usr/bin/script (PID=$$, after exec)
    participant claude as real claude (forked child, new PID)

    cmux->>wrapper: "spawn with stdin=pipe (non-TTY)"
    wrapper->>wrapper: "IN_CMUX=1, !-t 0, script probe OK"
    wrapper->>script: exec /usr/bin/script -q /dev/null REAL_CLAUDE [args]
    note over wrapper,script: wrapper process replaced — PID=$$ now tracks script, not claude
    script->>claude: fork+exec REAL_CLAUDE [args] in PTY slave
    claude-->>script: stdout/stderr via PTY master (ONLCR active)
    script-->>cmux: passthrough PTY output
    claude-->>script: exit(N)
    script-->>cmux: exit(N)
Loading

Reviews (3): Last reviewed commit: "Address PR feedback" | Re-trigger Greptile

Comment thread tests/test_claude_wrapper_hooks.py Outdated
Comment on lines +1085 to +1086
if not os.access("/usr/bin/script", os.X_OK):
return # BSD `script` is required for the workaround.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Opt-out path (CMUX_CLAUDE_FORCE_PTY=0) not tested

The comment in the wrapper documents CMUX_CLAUDE_FORCE_PTY=0 as the escape hatch for the PTY allocation. run_wrapper_pty_probe pops this variable from the environment to ensure the default path is exercised, but there is no corresponding test that sets CMUX_CLAUDE_FORCE_PTY=0 and asserts the real claude still sees a non-TTY stdin. If the condition logic regresses (e.g., the != "0" check is changed), the only way a breakage would surface is in production.

Comment thread Resources/bin/claude
Comment on lines +183 to +197
exec_real_claude() {
if [[ "${IN_CMUX:-0}" == "1" ]] \
&& [[ "${CMUX_CLAUDE_FORCE_PTY:-1}" != "0" ]] \
&& [[ ! -t 0 ]] \
&& [[ -x /usr/bin/script ]]; then
# BSD `script` calls tcgetattr on stdin and aborts when it cannot read
# attrs (some socket types). Probe with /usr/bin/true so a hostile
# stdin can never regress claude into "failed to start" — fall back to
# direct exec instead, preserving today's behavior.
if /usr/bin/script -q /dev/null /usr/bin/true >/dev/null 2>&1; then
exec /usr/bin/script -q /dev/null "$@"
fi
fi
exec "$@"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 PTY line-discipline processing may corrupt stdin data in automated invocations

A PTY's line discipline applies ONLCR (newline → CR+LF) and echo by default. In the common interactive case this is harmless, but the guard condition [[ ! -t 0 ]] fires precisely when stdin is a pipe or socket — i.e., the automated/scripted path. Any byte stream pumped into claude's stdin by cmux through that pipe (e.g., tool-result data, JSON payloads) passes through script's PTY master, where the line discipline can corrupt \n → \r\n before claude reads it. Consider calling stty -opost (or stty raw) on the PTY slave to suppress output processing if cmux ever uses stdio for structured data.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_claude_wrapper_hooks.py`:
- Around line 1085-1086: Replace the silent early return guarded by
os.access("/usr/bin/script", os.X_OK) with an explicit test skip so CI shows
skipped vs passed; specifically, where the code checks if not
os.access("/usr/bin/script", os.X_OK) change the behavior to call
pytest.skip("skipping test: /usr/bin/script missing (BSD script required)") (or
raise unittest.SkipTest) so the test runner records a visible skip rather than
silently returning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 37f885ff-0a3f-49ba-beae-19031d11ffbb

📥 Commits

Reviewing files that changed from the base of the PR and between 5829da2 and 7cb8043.

📒 Files selected for processing (2)
  • Resources/bin/claude
  • tests/test_claude_wrapper_hooks.py

Comment thread tests/test_claude_wrapper_hooks.py Outdated
@moaedy

moaedy commented May 12, 2026

Copy link
Copy Markdown
Author

Addressed feedback in f35e147: 1 review(s) requesting changes from coderabbitai[bot].

FOLLOW-UP SUMMARY:

  • tests/test_claude_wrapper_hooks.py:1085 — Replaced the silent early return (when /usr/bin/script is missing) with an explicit SKIP: print before returning. The reviewer asked for pytest.skip, but this file uses a custom test runner with failures: list[str] and no pytest dependency, so a print to stdout in the same style as the existing PASS: / FAIL: lines is th

@moaedy

moaedy commented May 12, 2026

Copy link
Copy Markdown
Author

Addressed feedback in 61e8d0d: 1 review(s) requesting changes from coderabbitai[bot]. FOLLOW-UP SUMMARY:

  • tests/test_claude_wrapper_hooks.py - Addressed greptile P2 "Opt-out path (CMUX_CLAUDE_FORCE_PTY=0) not tested". Extended run_wrapper_pty_probe with an optional force_pty parameter (still pops the env var by default so the existing default-path test is unchanged), then added test_force_pty_opt_out_skips_pty_allocation that drives the probe with force_pty="0" and ass

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_claude_wrapper_hooks.py`:
- Around line 1093-1100: The skip gate should check that the exact BSD-style
invocation used by the wrapper succeeds, not just that /usr/bin/script is
executable; in the test_inside_cmux_without_tty_allocates_pty_for_real_claude
replace the os.access check with running the same probe used by
exec_real_claude() ("/usr/bin/script -q /dev/null /usr/bin/true") via
subprocess.run and only skip if that probe fails (non-zero return) so the test
only runs when the BSD-script behavior is available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c333c6d5-e637-4df4-9b31-14970904c833

📥 Commits

Reviewing files that changed from the base of the PR and between f35e147 and 61e8d0d.

📒 Files selected for processing (1)
  • tests/test_claude_wrapper_hooks.py

Comment on lines +1093 to +1100
if not os.access("/usr/bin/script", os.X_OK):
# BSD `script` is required for the workaround; surface an explicit
# SKIP so CI logs distinguish skipped-on-Linux from passed-on-macOS.
print(
"SKIP: test_inside_cmux_without_tty_allocates_pty_for_real_claude "
"(requires BSD /usr/bin/script)"
)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ ! -x /usr/bin/script ]; then
  echo "SKIP_CONDITION_VALID: /usr/bin/script missing"
  exit 0
fi

if /usr/bin/script -q /dev/null /usr/bin/true >/dev/null 2>&1; then
  echo "BSD_STYLE_SCRIPT_SUPPORTED=1"
else
  echo "BSD_STYLE_SCRIPT_SUPPORTED=0"
fi

Repository: manaflow-ai/cmux

Length of output: 106


🏁 Script executed:

cat -n tests/test_claude_wrapper_hooks.py | sed -n '1090,1105p'

Repository: manaflow-ai/cmux

Length of output: 815


🏁 Script executed:

cat -n tests/test_claude_wrapper_hooks.py | sed -n '1084,1115p'

Repository: manaflow-ai/cmux

Length of output: 1612


🏁 Script executed:

cat -n tests/test_claude_wrapper_hooks.py | sed -n '988,1082p'

Repository: manaflow-ai/cmux

Length of output: 3731


🏁 Script executed:

rg -n "SOURCE_WRAPPER" tests/test_claude_wrapper_hooks.py | head -20

Repository: manaflow-ai/cmux

Length of output: 268


🏁 Script executed:

head -30 tests/test_claude_wrapper_hooks.py | grep -A2 "^ROOT"

Repository: manaflow-ai/cmux

Length of output: 159


🏁 Script executed:

cat Resources/bin/claude

Repository: manaflow-ai/cmux

Length of output: 17124


Gate this skip on script capability, not just executable presence.

The wrapper itself already probes /usr/bin/script -q /dev/null /usr/bin/true before using it (see exec_real_claude() in Resources/bin/claude). The test's skip gate should match: os.access("/usr/bin/script", os.X_OK) alone can pass on systems with non-BSD implementations, causing false test failures instead of skips. Probe the exact invocation before running assertions.

Suggested patch
-    if not os.access("/usr/bin/script", os.X_OK):
+    script_probe_ok = os.access("/usr/bin/script", os.X_OK)
+    if script_probe_ok:
+        probe = subprocess.run(
+            ["/usr/bin/script", "-q", "/dev/null", "/usr/bin/true"],
+            stdin=subprocess.DEVNULL,
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+            check=False,
+        )
+        script_probe_ok = probe.returncode == 0
+
+    if not script_probe_ok:
         # BSD `script` is required for the workaround; surface an explicit
         # SKIP so CI logs distinguish skipped-on-Linux from passed-on-macOS.
         print(
             "SKIP: test_inside_cmux_without_tty_allocates_pty_for_real_claude "
             "(requires BSD /usr/bin/script)"
         )
         return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_claude_wrapper_hooks.py` around lines 1093 - 1100, The skip gate
should check that the exact BSD-style invocation used by the wrapper succeeds,
not just that /usr/bin/script is executable; in the
test_inside_cmux_without_tty_allocates_pty_for_real_claude replace the os.access
check with running the same probe used by exec_real_claude() ("/usr/bin/script
-q /dev/null /usr/bin/true") via subprocess.run and only skip if that probe
fails (non-zero return) so the test only runs when the BSD-script behavior is
available.

@moaedy moaedy closed this May 13, 2026
@moaedy
moaedy deleted the godfix/issue-4001-run-13 branch May 13, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant