claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

Terminal state corruption after Ctrl+Z suspension - shell freezes, escape sequences displayed literally

Open risenowrise opened this issue 1 month ago • 3 comments

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)
    • ^Y and ^[ echoed verbatim
  • Cannot use normal shell commands
  • fg to 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:

  1. Call process.stdin.setRawMode(false) to restore cooked mode
  2. Restore original terminal settings (stty)
  3. 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);
  }
});

risenowrise avatar Nov 26 '25 16:11 risenowrise