Skip to main content

max / makenotwork

Return help text on shell request without a PTY A shell session with no PTY launched the TUI anyway, rendering into a terminal that does not exist and blocking forever on input that could never arrive. Non-interactive clients saw a hang instead of the usage help the command dispatcher advertises. Track whether the client requested a PTY and, when it did not, write help_text() and close the channel with exit status 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-19 19:18 UTC
Commit: a5fdabf2dfcf43cec23228c434d14eb99377576a
Parent: aa4ffec
2 files changed, +20 insertions, -1 deletion
@@ -517,7 +517,7 @@ pub async fn execute_pipe_upload(
517 517 ))
518 518 }
519 519
520 - fn help_text() -> Vec<u8> {
520 + pub fn help_text() -> Vec<u8> {
521 521 b"Usage: ssh cli.makenot.work <command>\r\n\
522 522 \r\n\
523 523 Commands:\r\n\
@@ -29,6 +29,9 @@ pub struct MnwHandler {
29 29 user: Option<UserInfo>,
30 30 /// Terminal dimensions (cols, rows).
31 31 term_size: (u16, u16),
32 + /// Whether the client requested a PTY. The TUI needs one; without it a
33 + /// shell session gets the help text instead of hanging on a dead terminal.
34 + pty_requested: bool,
32 35 /// TUI application handle for forwarding keypresses.
33 36 app: Option<tui::AppHandle>,
34 37 /// Channels stored between open and shell/subsystem request.
@@ -65,6 +68,7 @@ impl MnwHandler {
65 68 rate_limiter,
66 69 user: None,
67 70 term_size: (80, 24),
71 + pty_requested: false,
68 72 app: None,
69 73 channels: Arc::new(Mutex::new(HashMap::new())),
70 74 git_processes: HashMap::new(),
@@ -174,6 +178,7 @@ impl russh::server::Handler for MnwHandler {
174 178 _session: &mut Session,
175 179 ) -> Result<(), Self::Error> {
176 180 self.term_size = (col_width as u16, row_height as u16);
181 + self.pty_requested = true;
177 182 tracing::debug!(cols = col_width, rows = row_height, "PTY requested");
178 183 Ok(())
179 184 }
@@ -189,6 +194,20 @@ impl russh::server::Handler for MnwHandler {
189 194 return Ok(());
190 195 };
191 196
197 + // No PTY means no terminal to drive the TUI. Print the help text and
198 + // exit rather than launching a TUI that renders into nothing and
199 + // blocks forever waiting on input that can never arrive.
200 + if !self.pty_requested {
201 + tracing::info!(user = %user.username, "shell request without PTY, returning help");
202 + let handle = session.handle();
203 + let bytes = bytes::Bytes::from(crate::commands::help_text());
204 + let _ = handle.data(channel, bytes).await;
205 + let _ = handle.exit_status_request(channel, 0).await;
206 + let _ = handle.eof(channel).await;
207 + let _ = handle.close(channel).await;
208 + return Ok(());
209 + }
210 +
192 211 tracing::info!(user = %user.username, "launching TUI");
193 212
194 213 let handle = session.handle();