Repository navigation
Description
Environment
| Component | Version |
|---|---|
| happy-coder | 0.13.0 |
| Claude Code | 2.0.76 |
| Node.js | v22.19.0 |
| macOS | 26.2 (Build 25C56) |
| Terminal | xterm-24bit |
| Shell | zsh |
Summary
After switching from remote mode (mobile) back to local mode, terminal input becomes severely corrupted. Characters are dropped/eaten, the status line flashes erratically, and using slash commands (/) causes Claude Code CPU to spike to 100%, making the terminal completely unresponsive.
Steps to Reproduce
- Start happy-coder in local mode:
happy - Send a message from the mobile app (triggers automatic switch to remote mode)
- Press double-space to switch back to local mode
- Note: Often requires pressing space 3-5+ times before the switch actually works
- Attempt to type in local mode
Expected Behavior
- Double-space switches cleanly to local mode
- Terminal displays chat history normally
- Typing works normally with all characters appearing
Actual Behavior
- Multiple spaces required: Double-space often doesn't work; need 3-5+ presses
- Missing chat history: Terminal doesn't refresh properly, missing conversation context
- Character loss: Typed characters are dropped/eaten intermittently
- Status line flashing: Duration and context window values flash back and forth
- Slash command crash: Pressing
/causes Claude Code CPU to spike to 100% - Complete unresponsiveness: Terminal becomes frozen, requiring
killof both Claude Code and happy-coder processes
Additional Symptom
On exit, an EIO error is emitted:
EIO: i/o error, read
fd: 11,
syscall: "read",
errno: -5,
code: "EIO"
Root Cause Analysis
After investigating the source code, several issues were identified in the remote→local mode transition:
1. stdin state not properly reset after remote mode
In claudeRemoteLauncher finally block (~line 2980-2992 in index-B3gQr6vs.mjs):
} finally {
permissionHandler.reset();
process.stdin.off("data", abort);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
if (inkInstance) {
inkInstance.unmount();
}
// ...
}Problems:
stdin.pause()is never called - stdin remains in "flowing" modesetEncoding("utf8")set during remote mode setup (~line 2693) is never reset- Keypress listeners may not be fully removed
Compare to remote mode setup (~line 2689-2695):
if (hasTTY) {
process.stdin.resume();
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.setEncoding("utf8"); // This persists!
}2. Race condition with Ink unmount
When inkInstance.unmount() is called, Ink's internal stdin handling may not fully clean up before claudeLocal spawns the new Claude Code process with stdio: ["inherit"].
3. Async cleanup not awaited
In the main cleanup handler (~line 5210-5215):
stopCaffeinate();
happyServer.stop(); // Not awaited
hookServer.stop(); // Not awaited
cleanupHookSettingsFile(hookSettingsPath);
logger.debug("[START] Cleanup complete, exiting");
process.exit(0); // Exits while async operations still runningThis causes the EIO error on exit.
Attempted Fixes (Unsuccessful)
Added to the finally block:
process.stdin.removeAllListeners("keypress");
process.stdin.pause();This did not resolve the issue, suggesting the problem may be deeper - possibly in how Claude Code handles inherited stdin when the terminal is in an inconsistent state, or a race condition with Ink.
Suggested Investigation Areas
- Ink stdin handling: Check if Ink fully releases stdin control on unmount
- Claude Code terminal setup: How does Claude Code initialize terminal when resuming a session with inherited stdio?
- Stream encoding: The
setEncoding("utf8")call may affect how Claude Code reads raw terminal input - Timing: Add delays or proper awaits between Ink unmount and Claude spawn
Workaround
Currently none - the remote→local switch is essentially unusable. Users must avoid using mobile mode or restart happy-coder after each mobile interaction.
Log Reference
Relevant log entries showing the mode switch:
[01:02:37.816] [loop] Iteration with mode: remote
[01:02:58.064] [remote]: Switching to local mode via double space
[01:02:58.065] [remote]: doSwitch
[01:02:58.066] [remote]: launch finally
[01:02:58.076] [loop] Iteration with mode: local
[01:02:58.079] [local]: launch
[01:02:58.079] [ClaudeLocal] Will resume existing session: ...
The switch appears to complete in logs, but terminal state is corrupted.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
tonydehnke commentedon Jan 4, 2026
Experiencing this same issue. After switching from iPhone back to Mac, cursor input becomes extremely slow/laggy - to the point of needing to force quit the terminal session entirely.
The double-space switch often doesn't register on first try (need 3-5 presses as described). Once back in local mode, typing is nearly unusable.
Environment
This is a blocker for the mobile→desktop workflow. Would love to see this prioritized.
sstraus commentedon Jan 6, 2026
This is becoming really annoying. Someone found a solution?
gabimoncha commentedon Jan 6, 2026
i just installed the app and experienced too
JoshuaSeidel commentedon Jan 9, 2026
upvote
ShareOut commentedon Jan 12, 2026
Hi,
I ran into an issue where Claude Code processes can remain alive as orphans when happy switches modes (remote/local). These detached processes keep stdin inherited, which leads to corrupted terminal behavior (e.g. characters being randomly swallowed due to multiple processes reading from stdin).
On my system, this happens because termination signals are not propagated to the spawned Claude binary. As a result, killing the launcher does not terminate the child process.
I applied the following fix locally, which resolves the issue for me: propagate termination signals (SIGTERM, SIGINT, SIGHUP) to the spawned child process.
Location:
happy-cli/scripts/claude_version_utils.cjs (around line 362), inside the binary spawn branch.
After this code :
Put :
This works in practice because happy already recreates a fresh Claude instance on each remote/local switch.
I haven’t yet verified whether this fully aligns with the rest of the codebase or any intended lifecycle management, so feedback is very welcome.
Let me know what you think.
materemias commentedon Jan 13, 2026
This worked for me. Do you plan to submit a PR in happy-cli repo?
sferich888 commentedon Jan 20, 2026
Confirming on Linux (Fedora 43)
Experiencing the same terminal input corruption issues on Linux.
Environment
Notes
materemias commentedon Jan 20, 2026
this PR fixes it
sferich888 commentedon Jan 22, 2026
Is this then a duplicate of #430 ?
fix(terminal): harden remote→local switching input
fix(claude): forward signals to binary child process
fix: restore terminal state after remote→local mode switch
fix: restore terminal state after remote→local mode switch
fix: restore terminal state after remote→local mode switch
bra1nDump commentedon Apr 14, 2026
tracking this in #613 - same root cause (input handler persists after remote→local switch). closing here to keep discussion in one place.
fix: restore terminal state after remote→local mode switch
fix(cli): force blocking stdin before claude spawn + diagnostic logs
fix: restore terminal state after remote→local mode switch
chore(release): 1.2.0 [skip ci]
fix(cli): force blocking stdin before claude spawn + diagnostic logs
fix(cli): force blocking stdin before claude spawn + diagnostic logs