Terminal state corruption after Ctrl+Z suspension - shell freezes, escape sequences displayed literally
Description
After suspending Claude Code with Ctrl+Z (or Ctrl+Y depending on configuration), the terminal remains in raw mode. When returning to the shell, input is not interpreted correctly - the shell appears frozen and escape sequences are displayed literally instead of being processed.
Symptoms
- Shell appears frozen/unresponsive after suspension
- Escape sequences displayed literally:
-
^[[A(up arrow) -
^[[B(down arrow) -
^[[D(left arrow) -
^C(interrupt signal - not processed) -
^Yand^[echoed verbatim
-
- Cannot use normal shell commands
-
fgto resume Claude Code may not work properly
Example Session
Claude Code has been suspended. Run `fg` to bring Claude Code back.
Note: ctrl + z now suspends Claude Code, ctrl + _ undoes input.
^Y^Y^[^[^C
.
^[[A^[[A^[[A^[[B^[[B^[[B^[[B^[[B^[[D^[[D^C^C
Expected Behavior
When Claude Code is suspended (SIGTSTP), the terminal should be restored to its previous state (cooked mode with proper line editing), allowing normal shell interaction until fg is used to resume.
Root Cause Analysis
Claude Code uses raw terminal mode for interactive input handling. When receiving SIGTSTP (suspend signal), the process should:
- Call
process.stdin.setRawMode(false)to restore cooked mode - Restore original terminal settings (stty)
- Then allow the process to suspend
Currently, it appears the SIGTSTP handler is not restoring terminal state before suspension.
Workaround
When stuck in this state, type blindly and press Enter:
stty sane
Or:
reset
Environment
- Shell: Fish (but likely affects all shells)
- Terminal: Ghostty (but likely affects all terminals)
- OS: macOS (Darwin)
- Previously working: Yes - this is a regression
Technical Notes
The terminal state before/after suspension can be compared with:
stty -a > /tmp/tty-before.txt # Before running Claude Code
# ... suspend ...
stty -a > /tmp/tty-after.txt # After suspension (type blind)
diff /tmp/tty-before.txt /tmp/tty-after.txt
Key settings that are likely incorrect after suspension:
-
icanon(canonical mode) - should be ON for shell -
echo- should be ON for shell -
icrnl(CR to NL mapping) - should be ON -
-raw- should NOT be in raw mode
Suggested Fix
In the SIGTSTP signal handler, before calling process.kill(process.pid, 'SIGTSTP'):
process.on('SIGTSTP', () => {
// Restore terminal state before suspending
if (process.stdin.isTTY && process.stdin.isRaw) {
process.stdin.setRawMode(false);
}
// Optionally restore full stty settings if saved
process.kill(process.pid, 'SIGTSTP');
});
process.on('SIGCONT', () => {
// Re-enable raw mode when resumed
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
});