Ctrl+Z wedges the TUI when the process group is orphaned: raw mode is surrendered for a stop the kernel then discards
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:
- Claude restores cooked terminal modes, handing the tty back.
- Claude raises the stop on itself.
- The kernel discards it. Nothing stops.
- 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
- 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.
- Open the tab and let Claude start.
- 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, neverT. 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:
- 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".
- 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.
- 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-isigis off. Claude is not using it.- Adding
-ito 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 typingfg. It does not stop the interruption.- Swallowing the key in Windows Terminal - the only thing that works today, because
-isigmeans 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.