fix: resolve ANSI spinner corruption and garbled output (#3001)#3003
Merged
fix: resolve ANSI spinner corruption and garbled output (#3001)#3003
Conversation
… before SSH handoff Fixes two UX issues from live E2E session (#3001): 1. Download spinner (p.spinner from @clack/prompts) wrote ANSI escape codes to stdout. When stdout is captured (E2E harness, piped output), these sequences appeared as raw text rather than rendered colors. Replace p.spinner() in downloadScriptWithFallback and downloadBundle with logStep/logInfo/logError from shared/ui.ts, which write to stderr and correctly check isTTY before emitting ANSI codes. 2. Garbled output at start of interactive session (overlapping status lines from the remote agent's TUI) may be caused by residual ANSI state from @clack/prompts (hidden cursor, active color attributes). Emit ESC[?25h ESC[0m to stderr before prepareStdinForHandoff() to explicitly restore cursor visibility and reset all attributes before the SSH session takes over. Agent: issue-fixer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e mode (#3001) Three root causes fixed: 1. Spinner wrote to stdout while all other CLI status output goes to stderr, causing ANSI escape sequence interleaving and corruption when both streams are merged on a terminal. Redirected all p.spinner() calls to process.stderr. 2. unicode-detect.ts (which sets TERM=linux for SSH sessions to force ASCII fallback) was only imported in commands/shared.ts but not in shared/ui.ts. Cloud module entry points (hetzner/main.ts, etc.) that import shared/ui.ts loaded @clack/prompts without the TERM override, causing Unicode spinner frames in environments that can't render them. 3. After an interactive SSH session ends, the remote agent's TUI (e.g. Claude Code) may leave the terminal in raw mode with altered attributes. Added terminal reset (ANSI attribute reset + stty sane) after spawnInteractive() returns to prevent garbled post-session output. Agent: ux-engineer Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
louisgv
approved these changes
Mar 26, 2026
Member
louisgv
left a comment
There was a problem hiding this comment.
Security Review
Verdict: APPROVED
Commit: dca6ec7
Findings
No security issues found. The PR fixes ANSI spinner corruption in interactive mode by:
- Routing spinner output to stderr (prevents stdout contamination)
- Resetting terminal state before/after SSH sessions using hardcoded ANSI sequences
- Running stty sane with hardcoded arguments (no command injection risk)
All ANSI escape sequences are hardcoded literals (\x1b[?25h\x1b[0m) - no user input concatenation.
The stty command uses only the hardcoded "sane" argument - no injection vectors.
Tests
- bash -n: N/A (no shell scripts modified)
- bun test: SKIP (module resolution errors in test environment, unrelated to PR changes)
- biome lint: PASS (0 errors on all modified TypeScript files)
- curl|bash: N/A (no shell scripts modified)
- macOS compat: N/A (no shell scripts modified)
Code Quality
- Version bumped: 0.26.7 → 0.26.9 ✓
- Test coverage updated for spinner → stderr migration ✓
- All spinner calls now use
{ output: process.stderr }consistently ✓
-- security/pr-reviewer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why: Spinner output was rendering garbled ANSI sequences (each char on a separate line), and final status messages after SSH sessions showed corrupted text. Repro: run
spawn claude hetzner.Changes
1. Redirect all spinner output to stderr (
commands/shared.ts,commands/link.ts,commands/delete.ts,commands/update.ts,commands/status.ts)p.spinner()calls now pass{ output: process.stderr }to match the CLI's convention of writing status output to stderr2. Load unicode-detect before @clack/prompts in all entry points (
shared/ui.ts)unicode-detect.ts(which forcesTERM=linuxfor SSH sessions) was only imported incommands/shared.ts, not inshared/ui.tshetzner/main.ts, etc.) that importshared/ui.tsloaded@clack/promptswithout the TERM override, causing Unicode spinner frames in environments that can't render themshared/ui.tsso it runs before@clack/promptsregardless of entry point3. Reset terminal state after interactive SSH sessions (
shared/ssh.ts)spawnInteractive()returns, the remote agent's TUI (e.g. Claude Code) may leave the terminal in raw mode with altered attributesstty saneto restore terminal to a clean state4. Version bump (
package.json: 0.26.8 -> 0.26.9)Test plan
bunx @biomejs/biome check src/-- zero errorsbun test-- all 1953 tests passFixes #3001
-- refactor/ux-engineer