Ctrl+Z wedges the TUI when the process group is orphaned: raw mode is surrendered for a stop the kernel then discards

Status Open
Reported on v2.1.241
Maintainer reply None cached
Activity 0 comments · opened Oct 12, 2026

Summary

Claude Code handles Ctrl+Z itself. On a Claude tty, -isig is set, so the kernel never generates SIGTSTP - the TUI reads byte 0x1A and self-suspends in software.

The self-suspend assumes a job-control shell is waiting. When Claude is launched with no such shell, its process group is orphaned, and POSIX requires the kernel to discard stop signals sent to an orphaned process group. So the sequence is:

  1. Claude restores cooked terminal modes, handing the tty back.
  2. Claude raises the stop on itself.
  3. The kernel discards it. Nothing stops.
  4. Claude is alive and drawing, holding a cooked tty, reading no keys.

The terminal is given away for a suspend that can never happen. There is no SIGCONT and no shell prompt, so the normal fg recovery does not exist either.

Environment

  • Claude Code 2.1.241
  • WSL 2.7.10.0, Ubuntu 24.04.3 LTS, kernel 6.18.33.2-microsoft-standard-WSL2
  • Windows Terminal, launching Claude directly from a profile (no shell in between)
  • Profile commandline: wsl.exe -d Ubuntu-24.04 -- bash -lc "cd '<project>' && claude -n '<name>' '/session-start'"

This launch pattern is common: it is what Windows Terminal profiles and one-click launchers produce.

Reproduction

  1. Create a Windows Terminal profile whose commandline is wsl.exe -d <distro> -- bash -lc "claude".

bash -lc is non-interactive, has job control off, and execs itself away, so Claude ends up with no shell parent.

  1. Open the tab and let Claude start.
  2. Press Ctrl+Z.

Expected: either a real suspend that fg can resume, or Ctrl+Z is ignored and the TUI keeps working.

Actual: the TUI stops accepting input. The process is still alive and still redrawing. There is no [1]+ Stopped message and no shell prompt, because there is no shell.

Evidence

Process state, taken from outside the affected session:

    PID    PPID    PGID     SID STAT TT       CMD
    349     348     349     349 Ssl+ pts/0    claude -n 2026Japan /session-start
    348     347     347     347 S            /init
  • Claude is PGID 349 = SID 349, so it leads its own process group and session.
  • Its parent 348 is /init, sitting in a different session (347).
  • That is the definition of an orphaned process group, so the kernel discards the stop.
  • State stays S, never T. The process never stops.

Terminal modes on the affected pty, against a healthy control:

healthy Claude tty:   -isig -icanon -iexten -echo   <- as the TUI set it
after Ctrl+Z:          isig  icanon  iexten  echo   <- cooked, given back to a shell that does not exist

The -isig on the healthy control is the key point: the kernel cannot have produced SIGTSTP, so the suspend is Claude's own software behaviour.

Workaround

Recoverable from outside, with no restart and no loss of context:

kill -CONT <pid> && kill -WINCH <pid>

If the modes do not come back, force them and nudge again:

stty -F /dev/pts/<N> raw -echo && kill -CONT <pid> && kill -WINCH <pid>

SIGWINCH is what forces the redraw. stty raw covers -icrnl, which matters - see #88249, where restoring only -isig -icanon -iexten -echo produces a convincing but still broken state in which Enter does nothing.

Proposed fix

Preferred, in order:

  1. Do not surrender the terminal for a stop that cannot take effect. Before restoring cooked modes, check whether the process group is orphaned (parent's session differs from own, or no job-control shell owns the tty). If it is, skip the suspend and keep raw mode. Optionally show a one-line notice: "no job-control shell to return to, suspend ignored".
  2. Restore raw mode if the stop does not take. After raising the stop, if execution continues immediately then the signal was discarded - re-apply the terminal modes rather than leaving them cooked.
  3. Make Ctrl+Z configurable. A setting to ignore the key entirely. This was asked for in #43596 (closed) and is the only fix a user can apply today, and only outside the terminal - see Alternatives.

Fixes 1 and 2 are small, local, and do not change behaviour for anyone who has a normal job-control shell.

Alternatives tried, and why they fall short

  • stty susp undef - does nothing. The susp character is already inert because -isig is off. Claude is not using it.
  • Adding -i to the launcher (bash -lic) - does not help. Bash still execs itself away, so there is still no shell parent.
  • bash -lic "claude; :" - the trailing ; : does defeat the exec optimisation and leaves a live shell parent, so the pgrp is no longer orphaned. But it only converts the hang into a suspend that still drops the user out of the TUI and requires typing fg. It does not stop the interruption.
  • Swallowing the key in Windows Terminal - the only thing that works today, because -isig means every layer below the terminal emulator has already given up the ability to intercept it:

``json
"actions": [ { "command": { "action": "sendInput", "input": "" }, "id": "User.swallowCtrlZ" } ],
"keybindings": [ { "id": "User.swallowCtrlZ", "keys": "ctrl+z" } ]
``
Windows Terminal keybindings are application-global, not per-profile, so this removes Ctrl+Z from every tab, including non-Claude ones. Deliberate suspend of vim or a long-running command is lost as collateral.

Related issues, disclosed up front

  • #88249 (open) - same wedged end state, different cause. There job control works and the TUI fails to restore modes after SIGCONT/fg. Here the stop is discarded, so there is no SIGCONT at all. The resume-side fix in that issue would not help this case, but a shared "always re-assert terminal modes" routine would fix both.
  • #31264 (closed, stale) - closest root cause on record: "Ctrl+Z sends SIGSTOP to own PID instead of SIGTSTP to process group, breaks job control in containers/sandboxes". Same family, reported for containers. This report is the WSL2 and Windows Terminal instance of it, with the orphaned-process-group mechanism identified.
  • #43596 (closed, enhancement) - "Ctrl+Z suspends CLI, should be intercepted or remapped". That is proposed fix 3 here.
  • #36976 (closed, stale) - "Ctrl+Z suspend has no reliable resume, destroys long-running agent sessions", platform:linux.
  • #75724 (closed, duplicate) - WSL2 keyboard freeze needing Ctrl-Z recovery. Same platform, adjacent symptom.
  • #78690 (closed) - TUI does not restore terminal state on SIGTSTP.
  • #57254 (closed, stale) - "Claude Code suspends with SIGTTIN/SIGTTOU on launch" on macOS. Same job-control-signal family, different signal and platform.
  • #65486 (closed, duplicate), #46211, #17045, #16727 - the wider Ctrl+Z cluster.

This is not a duplicate of any single one of the above. The distinct claim is narrow and testable: when the process group is orphaned the stop is discarded, so restoring cooked modes before raising it is unconditionally wrong.

View original on GitHub ↗