You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After an interactive bssh PTY session ends, the local terminal can remain in an enhanced keyboard-reporting mode that was enabled by a remote application. Subsequent key presses at the local shell are then emitted as raw control-sequence fragments instead of normal input.
Observed examples on Ghostty 1.3.1 with TERM=xterm-ghostty:
Space -> ;1:3u
Right arrow -> :1C:3C
The complete input sequences are consistent with Kitty keyboard protocol / CSI-u events such as:
CSI 32;1:3u # Space, no modifiers, key release
CSI 1;1:1C # Right arrow, no modifiers, key press
CSI 1;1:3C # Right arrow, no modifiers, key release
The local shell or line editor may consume the leading ESC [ and part of the parameters, leaving only fragments such as ;1:3u or :1C:3C visible.
This state persists after bssh returns to the local prompt and usually requires reset, an explicit Kitty keyboard-mode reset, or opening a new terminal tab/window.
Root Cause
Interactive terminal protocols intentionally allow applications to change how the terminal emulator reports keyboard events. Remote applications such as Neovim, Helix, Yazi, fish, nushell, and crossterm-based TUIs may enable Kitty keyboard progressive enhancements by emitting sequences such as:
CSI > flags u
Those bytes travel through the SSH PTY and are interpreted by the local terminal emulator. The terminal then reports key presses, repeats, and releases using CSI-u sequences until the application restores the previous mode with CSI < u or resets the active flags.
When the remote application or SSH transport terminates before sending its matching cleanup sequence, the local terminal remains in the enhanced keyboard mode. A network drop, remote process crash, forced disconnect, or application that enables the mode without balancing it can all trigger this condition.
This behavior is documented by the Kitty keyboard protocol and Neovim:
Kitty keyboard progressive-enhancement flags or stack (CSI < ... u / CSI = ... u)
xterm modifyOtherKeys, which Neovim uses as a fallback when CSI-u is unavailable
Therefore cleanup executes after ordinary EOF and network-loss paths, but it is incomplete for enhanced keyboard protocols.
TerminalState::save_terminal_state() also currently contains a TODO and assumes that bssh starts from a clean terminal state. A blind reset to legacy keyboard mode would fix the common shell case but could overwrite a keyboard mode owned by an outer local TUI that launched bssh.
Deterministic Reproduction
This can be reproduced without depending on a particular remote editor:
Start an interactive PTY session from a Kitty-keyboard-compatible terminal:
bssh user@host
From the remote shell, enable all crossterm-supported Kitty keyboard enhancements and immediately exit without balancing the push:
printf'\033[>15u';exit
After bssh returns to the local shell, press Space and the arrow keys.
Observe CSI-u fragments such as ;1:3u, :1C, or :3C instead of normal key behavior.
Run reset or open a new terminal tab to recover.
A realistic abrupt-disconnect reproduction is to start a remote Kitty-keyboard-aware TUI, verify that enhanced event reporting is active, and then terminate the SSH transport or remote PTY process before the application exits cleanly.
Proposed Solution
Extend the centralized terminal teardown so that bssh restores enhanced keyboard modes on every cleanup path.
1. Centralize terminal reset emission
Extract the repeated escape-sequence writes from TerminalStateGuard::restore_terminal_state() and force_terminal_cleanup() into a shared best-effort helper that writes to an injectable Write target. Both normal RAII cleanup and panic/last-resort cleanup must use the same sequence set.
The helper must remain idempotent, non-panicking, and best-effort. Preserve the current try_lock() behavior in force_terminal_cleanup() so panic cleanup cannot deadlock on TERMINAL_MUTEX.
2. Restore Kitty keyboard state
At minimum, teardown should ensure that stale Kitty keyboard progressive enhancements no longer affect the shell after bssh exits. Candidate sequences include popping outstanding keyboard-mode entries and explicitly clearing active flags:
CSI < number u
CSI = 0 u
The implementation must account for the Kitty protocol maintaining separate keyboard-mode stacks for the main and alternate screens. Cleanup may need to restore the current screen before leaving alternate-screen mode and restore the main screen again afterward.
3. Preserve the pre-existing local mode where possible
The robust design should query and record the local Kitty keyboard flags before handing terminal input to the remote PTY:
CSI ? u
On teardown, restore the recorded flags rather than always forcing legacy mode. This prevents bssh from breaking an outer local TUI that already owns an enhanced keyboard mode.
If exact state preservation cannot be implemented safely in the first patch, use a clearly documented best-effort fallback for ordinary shell sessions and track exact state preservation separately. Avoid silently claiming that TerminalState is fully preserved while save_terminal_state() still assumes a clean baseline.
4. Reset the xterm fallback
Also restore xterm modifyOtherKeys when it may have been enabled by a remote application. Neovim uses CSI > 4 ; 2 m as the fallback when CSI-u support is unavailable. The teardown should emit the corresponding disable/reset sequence on terminals that use this path.
5. Do not filter valid enable sequences during the session
Do not solve this by stripping Kitty or modifyOtherKeys control sequences from remote output. Interactive remote applications require these sequences for correct key handling. The correct boundary is session teardown: allow terminal protocols during the PTY session, then restore the local terminal state when ownership returns to the local shell.
Acceptance Criteria
After normal remote EOF/Close, the local terminal accepts Space, arrow keys, Enter, and modifier combinations normally.
After an abrupt SSH transport loss while an enhanced-keyboard-aware application is active, no CSI-u fragments such as ;1:3u, :1C, or :3C appear at the local shell.
Local disconnect through the supported ~. escape restores keyboard state.
Input-send errors and internal PTY error paths restore keyboard state.
Both TerminalStateGuard::Drop and force_terminal_cleanup() use the same keyboard cleanup behavior.
Kitty keyboard state is restored on both the alternate and main screen paths.
xterm modifyOtherKeys is disabled or restored when it was enabled through the fallback protocol.
Cleanup remains idempotent, best-effort, panic-safe, and safe to call more than once.
A pre-existing enhanced keyboard mode owned by an outer local application is preserved where the terminal supports querying/restoring it.
Non-supporting terminal emulators ignore the added sequences without visible artifacts or regressions.
Non-PTY commands and traditional interactive mode do not regress.
Automated tests verify the exact reset bytes through an injectable writer instead of relying only on captured process stdout.
A real-TTY integration or documented manual test covers Ghostty and at least one additional Kitty-keyboard-compatible terminal.
Out of Scope
No in-process cleanup can run after local SIGKILL, immediate process abort without unwinding, terminal emulator termination, power loss, or kernel failure. Those cases still require terminal-level recovery such as reset or opening a new terminal instance.
#189 is the closest precedent: remote applications could leave mouse tracking enabled after disconnect, and bssh fixed it by extending the centralized teardown. Enhanced keyboard modes require the same class of cleanup, with additional care to preserve any pre-existing local keyboard state.
Problem / Background
After an interactive
bsshPTY session ends, the local terminal can remain in an enhanced keyboard-reporting mode that was enabled by a remote application. Subsequent key presses at the local shell are then emitted as raw control-sequence fragments instead of normal input.Observed examples on Ghostty 1.3.1 with
TERM=xterm-ghostty:The complete input sequences are consistent with Kitty keyboard protocol / CSI-u events such as:
The local shell or line editor may consume the leading
ESC [and part of the parameters, leaving only fragments such as;1:3uor:1C:3Cvisible.This state persists after
bsshreturns to the local prompt and usually requiresreset, an explicit Kitty keyboard-mode reset, or opening a new terminal tab/window.Root Cause
Interactive terminal protocols intentionally allow applications to change how the terminal emulator reports keyboard events. Remote applications such as Neovim, Helix, Yazi, fish, nushell, and crossterm-based TUIs may enable Kitty keyboard progressive enhancements by emitting sequences such as:
Those bytes travel through the SSH PTY and are interpreted by the local terminal emulator. The terminal then reports key presses, repeats, and releases using CSI-u sequences until the application restores the previous mode with
CSI < uor resets the active flags.When the remote application or SSH transport terminates before sending its matching cleanup sequence, the local terminal remains in the enhanced keyboard mode. A network drop, remote process crash, forced disconnect, or application that enables the mode without balancing it can all trigger this condition.
This behavior is documented by the Kitty keyboard protocol and Neovim:
Neovim explicitly notes that an unclean exit can leave the terminal in a bad state and recommends
resetas recovery.Code Analysis
bsshcorrectly detects several PTY termination conditions insrc/pty/session/session_manager.rs:ChannelMsg::EofandChannelMsg::CloseChannel::wait()returningNoneAfter the session loop,
TerminalStateGuardis dropped and the higher-level PTY paths also callforce_terminal_cleanup().However, both cleanup implementations in
src/pty/terminal.rsrestore only a subset of terminal state:The emitted cleanup blob currently contains:
b"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?1049l\x1b[?25h"It does not restore any of the following:
CSI < ... u/CSI = ... u)modifyOtherKeys, which Neovim uses as a fallback when CSI-u is unavailableTherefore cleanup executes after ordinary EOF and network-loss paths, but it is incomplete for enhanced keyboard protocols.
TerminalState::save_terminal_state()also currently contains a TODO and assumes thatbsshstarts from a clean terminal state. A blind reset to legacy keyboard mode would fix the common shell case but could overwrite a keyboard mode owned by an outer local TUI that launchedbssh.Deterministic Reproduction
This can be reproduced without depending on a particular remote editor:
Start an interactive PTY session from a Kitty-keyboard-compatible terminal:
From the remote shell, enable all crossterm-supported Kitty keyboard enhancements and immediately exit without balancing the push:
After
bsshreturns to the local shell, press Space and the arrow keys.Observe CSI-u fragments such as
;1:3u,:1C, or:3Cinstead of normal key behavior.Run
resetor open a new terminal tab to recover.A realistic abrupt-disconnect reproduction is to start a remote Kitty-keyboard-aware TUI, verify that enhanced event reporting is active, and then terminate the SSH transport or remote PTY process before the application exits cleanly.
Proposed Solution
Extend the centralized terminal teardown so that
bsshrestores enhanced keyboard modes on every cleanup path.1. Centralize terminal reset emission
Extract the repeated escape-sequence writes from
TerminalStateGuard::restore_terminal_state()andforce_terminal_cleanup()into a shared best-effort helper that writes to an injectableWritetarget. Both normal RAII cleanup and panic/last-resort cleanup must use the same sequence set.The helper must remain idempotent, non-panicking, and best-effort. Preserve the current
try_lock()behavior inforce_terminal_cleanup()so panic cleanup cannot deadlock onTERMINAL_MUTEX.2. Restore Kitty keyboard state
At minimum, teardown should ensure that stale Kitty keyboard progressive enhancements no longer affect the shell after
bsshexits. Candidate sequences include popping outstanding keyboard-mode entries and explicitly clearing active flags:The implementation must account for the Kitty protocol maintaining separate keyboard-mode stacks for the main and alternate screens. Cleanup may need to restore the current screen before leaving alternate-screen mode and restore the main screen again afterward.
3. Preserve the pre-existing local mode where possible
The robust design should query and record the local Kitty keyboard flags before handing terminal input to the remote PTY:
On teardown, restore the recorded flags rather than always forcing legacy mode. This prevents
bsshfrom breaking an outer local TUI that already owns an enhanced keyboard mode.If exact state preservation cannot be implemented safely in the first patch, use a clearly documented best-effort fallback for ordinary shell sessions and track exact state preservation separately. Avoid silently claiming that
TerminalStateis fully preserved whilesave_terminal_state()still assumes a clean baseline.4. Reset the xterm fallback
Also restore xterm
modifyOtherKeyswhen it may have been enabled by a remote application. Neovim usesCSI > 4 ; 2 mas the fallback when CSI-u support is unavailable. The teardown should emit the corresponding disable/reset sequence on terminals that use this path.Reference:
5. Do not filter valid enable sequences during the session
Do not solve this by stripping Kitty or
modifyOtherKeyscontrol sequences from remote output. Interactive remote applications require these sequences for correct key handling. The correct boundary is session teardown: allow terminal protocols during the PTY session, then restore the local terminal state when ownership returns to the local shell.Acceptance Criteria
;1:3u,:1C, or:3Cappear at the local shell.~.escape restores keyboard state.TerminalStateGuard::Dropandforce_terminal_cleanup()use the same keyboard cleanup behavior.modifyOtherKeysis disabled or restored when it was enabled through the fallback protocol.Out of Scope
No in-process cleanup can run after local
SIGKILL, immediate process abort without unwinding, terminal emulator termination, power loss, or kernel failure. Those cases still require terminal-level recovery such asresetor opening a new terminal instance.Related Issues
#189 is the closest precedent: remote applications could leave mouse tracking enabled after disconnect, and
bsshfixed it by extending the centralized teardown. Enhanced keyboard modes require the same class of cleanup, with additional care to preserve any pre-existing local keyboard state.