Skip to content

fix(tmux): restore Ctrl+Z suspend/resume inside attached sessions#328

Open
rhukster wants to merge 1 commit intoasheshgoplani:mainfrom
rhukster:fix/ctrl-z-suspend-in-tmux
Open

fix(tmux): restore Ctrl+Z suspend/resume inside attached sessions#328
rhukster wants to merge 1 commit intoasheshgoplani:mainfrom
rhukster:fix/ctrl-z-suspend-in-tmux

Conversation

@rhukster
Copy link
Contributor

@rhukster rhukster commented Mar 12, 2026

Summary

Ctrl+Z (suspend) has been broken inside agent-deck tmux sessions, leaving users in an unrecoverable frozen terminal state where control characters echo as raw text (^Z^Z^Z^Z). Only Ctrl+Q (detach) still worked. This PR fixes two independent root causes:

Problem 1: PTY line discipline suspending the tmux client

The PTY master fd connecting agent-deck to tmux attach-session had default terminal settings with ISIG enabled. When the user pressed Ctrl+Z, the PTY's line discipline interpreted byte 0x1A as SUSP and delivered SIGTSTP to the tmux attach process — suspending the tmux client. This left the user in a frozen limbo state: no process was actively reading input, so further control characters were simply echoed as text. Only Ctrl+Q escaped because it is intercepted in the stdin-reader goroutine before bytes reach the PTY.

Fix: Set the PTY master to raw mode (term.MakeRaw) immediately after creation, so all bytes pass through transparently to tmux. This is safe because:

  • The PTY master fd is fully isolated from os.Stdin/os.Stdout and Bubble Tea's terminal state
  • tmux expects to receive raw bytes and routes control characters to panes internally
  • The existing Ctrl+Q interception in the stdin-reader goroutine is unaffected (it operates before bytes reach the PTY)

Problem 2: Non-interactive bash -c wrapper breaking job control

Compound session commands (containing $() subshells for UUID generation) are wrapped in bash -c '...' for fish shell compatibility. This non-interactive bash shell has no job control. When Claude Code handled Ctrl+Z internally by sending SIGTSTP to itself, the bash -c parent couldn't handle the stopped child — leaving the pane in a broken state.

Additionally, wrapIgnoreSuspend wrapped every command in bash -c 'stty susp undef; ...', which disabled the terminal's ability to generate SIGTSTP from Ctrl+Z.

Fix (two parts):

  • Add exec before the final claude invocation in compound commands. This replaces the bash -c process with claude, making it a direct child of the interactive shell where job control works naturally (Ctrl+Z → [1]+ Stopped → shell prompt → fg to resume).
  • Only apply wrapIgnoreSuspend for sandboxed sessions, where the command runs as the pane's initial process with no interactive shell for job control. Non-sandbox sessions launch via send-keys into the user's interactive shell, so standard job control works without intervention.

Test plan

  • Create a new non-sandbox Claude session, attach, press Ctrl+Z — Claude should suspend and show a shell prompt
  • Type fg — Claude should resume normally
  • Ctrl+Q still detaches back to the session list
  • Ctrl+C inside an attached session passes through to the running process (not intercepted by the PTY)
  • Sandboxed sessions still function correctly (wrapIgnoreSuspend still applied)
  • Fork and new-session-with-message flows work correctly with the exec prefix
  • Fish shell users can still create/resume sessions (bash -c wrapping still applied)

Ctrl+Z was broken inside agent-deck tmux sessions in two independent
ways, both preventing proper job control (suspend + fg resume):

1. The PTY master connecting agent-deck to `tmux attach-session` had
   default terminal settings with ISIG enabled. When Ctrl+Z (byte 0x1A)
   was written to the PTY, the kernel line discipline interpreted it as
   SUSP and delivered SIGTSTP to the tmux attach process — causing it to
   exit and returning the user to the session list (identical to Ctrl+Q).
   Fix: set the PTY master to raw mode so all bytes pass through
   transparently to tmux.

2. Compound session commands (containing $() subshells) are wrapped in
   `bash -c` for fish shell compatibility. This non-interactive bash has
   no job control, so when Claude Code sent SIGTSTP to itself, the
   bash -c parent couldn't handle the stopped child — leaving the
   terminal in an unrecoverable state. Two changes fix this:
   - Add `exec` before the final claude invocation in compound commands,
     so claude replaces the bash -c process and becomes a direct child
     of the interactive shell where job control works.
   - Only apply `wrapIgnoreSuspend` (stty susp undef) for sandboxed
     sessions where the command runs as the pane's initial process with
     no interactive shell. Non-sandbox sessions use send-keys into the
     user's shell, so Ctrl+Z works naturally.

Also removes a duplicate stripControlCharsPreserveANSI function
introduced by a merge.
@rhukster rhukster force-pushed the fix/ctrl-z-suspend-in-tmux branch from 47bd8ac to a5a945d Compare March 12, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant