From 4be57f099641e5b61bc7fd3c851542a65bb93e87 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 18 Jul 2026 05:53:16 +0200 Subject: [PATCH] feat(zsh): add claude() wrapper to survive early terminal input Typing during Claude Code's ~5s startup triggered a SIGTTIN job-control stop ("suspended (tty input)") that left mouse tracking / terminal probes enabled, spamming SGR mouse (35;44;41M) and DSR (997;1n) sequences. Upstream bug, closed "not planned" (anthropics/claude-code #7807 #11898 #23581 #50032); no flag/env disables the probes. Wrapper runs claude with job-control monitoring off (nomonitor) so it can't take SIGTTIN, and resets mouse/focus/paste/cursor modes to /dev/tty on exit as a self-heal. Opt-out via CLAUDE_TTY_FIX_OFF=1. Auto-loaded via an oh-my-zsh custom symlink (same pattern as calc-live.zsh); .zshrc untouched. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012fwBQdNPJPgKR3gcDZLRjo --- linux_configuration/zsh/claude-tty-fix.zsh | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 linux_configuration/zsh/claude-tty-fix.zsh diff --git a/linux_configuration/zsh/claude-tty-fix.zsh b/linux_configuration/zsh/claude-tty-fix.zsh new file mode 100644 index 00000000..af23e08c --- /dev/null +++ b/linux_configuration/zsh/claude-tty-fix.zsh @@ -0,0 +1,58 @@ +# ============================================================================ +# claude-tty-fix.zsh — make Claude Code survive early typing in a fresh terminal +# ---------------------------------------------------------------------------- +# Symptom this fixes: +# Launch `claude` in a fresh terminal and type/interact during its ~5s startup +# (instead of waiting) and the process stops with +# [1] + suspended (tty input) +# while the terminal spams sequences like `35;44;41M` (SGR mouse-motion +# reports) and `997;1n` (the reply to Claude's dark-mode color-scheme probe). +# +# Why it happens (upstream, not our config): +# Claude Code enables mouse tracking (\e[?1003h) and fires terminal-capability +# probes the instant it starts. `suspended (tty input)` is a SIGTTIN job-control +# stop: if the process is stopped mid-init, those terminal modes are never torn +# down, so the shell — back in the foreground — echoes mouse movement and probe +# replies as garbage. Known Claude Code bug, closed "not planned": +# anthropics/claude-code #7807, #11898, #23581, #50032. No CLI flag / env var / +# setting disables the mouse tracking or the probes, so we wrap the command. +# +# The wrapper does two things: +# 1. PREVENT the stop — run claude with zsh job-control monitoring off, so zsh +# does not put it in its own background process group and it can never take +# SIGTTIN. This is the only lever available outside the binary; whether it +# fully prevents the stop is validated empirically (see the plan). Trade-off: +# you cannot Ctrl-Z-suspend a claude session while this is active. +# 2. SELF-HEAL — on exit (however claude ended), disable the terminal modes it +# may have left on and reset the line discipline, so a garble never persists. +# +# Opt-out: `export CLAUDE_TTY_FIX_OFF=1` (falls back to the bare binary). +# ============================================================================ + +claude() { + # Escape sequences that undo the terminal state Claude may leave enabled if it + # is stopped/killed before its own teardown runs: + # 1000/1002/1003/1006 mouse tracking, 1004 focus reporting, + # 2004 bracketed paste, 25 cursor visibility. + local -r _claude_tty_reset=$'\e[?1000l\e[?1002l\e[?1003l\e[?1006l\e[?1004l\e[?2004l\e[?25h' + + if [[ -n "${CLAUDE_TTY_FIX_OFF:-}" ]]; then + command claude "$@" + return $? + fi + + # `localoptions` confines the option change to this function; `nomonitor` + # disables job control so claude shares the shell's (foreground) process group. + setopt localoptions nomonitor + command claude "$@" + local -r rc=$? + + # Only heal a real terminal, and write the reset to /dev/tty (the controlling + # terminal) rather than stdout — otherwise headless/piped runs like + # `claude -p '...' | jq` would get escape codes injected into their output. + if [[ -t 1 || -t 2 ]]; then + printf '%s' "$_claude_tty_reset" > /dev/tty 2>/dev/null + stty sane < /dev/tty 2>/dev/null + fi + return $rc +}