repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
a061876
parent
543b45c
author
Eric Bower
date
2025-12-29 15:28:18 -0500 EST
fix: prevent terminal state corruption on detach

When zmx sessions are nested through SSH (host zmx -> ssh -> remote zmx),
the detach restore sequence was corrupting the outer session's terminal
state tracking in ghostty-vt.

The issue: when the remote zmx client detached, it sent clear screen and
home cursor sequences (\x1b[2J\x1b[H) which flowed through SSH to the
host's PTY. This caused the host's ghostty-vt to update its cursor
position, and since nothing followed to correct it, subsequent reattaches
to the host session would restore with incorrect cursor positioning.

The fix: remove clear screen and home cursor from the detach sequence.
We still reset terminal modes (mouse tracking, bracketed paste, alt
screen, etc.) to ensure the terminal is left in a sane state.

Trade-offs:
- On detach, users now see the session content instead of a cleared
  screen. This is arguably useful feedback showing what was happening.
- Nested zmx through SSH may still have cursor positioning issues since
  the restore's CUP sequence also flows through to the outer session.
  This is accepted as an edge case we cannot fully solve without
  breaking normal operation.
- The attach clear is kept because it provides a clean slate and any
  corruption it causes is immediately overwritten by the session restore.

Design principle: be conservative about terminal escape sequences sent
on detach since they persist and affect the outer terminal's state.
The session restore handles reconstruction, so we don't need to
"clean up" on the way out.

References: https://github.com/neurosnap/zmx/issues/31
1 files changed,  +6, -3
M src/main.zig
+6, -3
 1@@ -695,10 +695,12 @@ fn attach(daemon: *Daemon) !void {
 2         // Reset terminal modes on detach:
 3         // - Mouse: 1000=basic, 1002=button-event, 1003=any-event, 1006=SGR extended
 4         // - 2004=bracketed paste, 1004=focus events, 1049=alt screen
 5-        // - 25h=show cursor, 2J=clear screen, H=cursor home
 6+        // - 25h=show cursor
 7+        // NOTE: We intentionally do NOT clear screen or home cursor here because we dont
 8+        // want to corrupt any programs that rely on it including ghostty's session restore.
 9         const restore_seq = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l" ++
10             "\x1b[?2004l\x1b[?1004l\x1b[?1049l" ++
11-            "\x1b[?25h\x1b[2J\x1b[H";
12+            "\x1b[?25h";
13         _ = posix.write(posix.STDOUT_FILENO, restore_seq) catch {};
14     }
15 
16@@ -718,7 +720,8 @@ fn attach(daemon: *Daemon) !void {
17 
18     _ = c.tcsetattr(posix.STDIN_FILENO, c.TCSANOW, &raw_termios);
19 
20-    // Clear screen and move cursor to home before attaching
21+    // Clear screen before attaching. This provides a clean slate before
22+    // the session restore.
23     const clear_seq = "\x1b[2J\x1b[H";
24     _ = try posix.write(posix.STDOUT_FILENO, clear_seq);
25