Terminal Internals Footguns¶
Mistakes that break terminals, corrupt sessions, produce garbled output, or cause hard-to-debug behavior in automation and remote work.
1. Setting TERM to the Wrong Value¶
You SSH into a server and set TERM=xterm-256color because you want colors. The server does not have the xterm-256color terminfo entry. Now every program that queries terminal capabilities gets errors or falls back to dumb mode. vim cannot render properly, less shows garbage, and clear does nothing.
Fix: TERM must match a terminfo entry installed on the machine running the program. Check with infocmp "$TERM". If it fails, use a safe fallback:
Inside tmux, use screen-256color or tmux-256color. Inside screen, use screen-256color. Never override TERM in your shell profile without checking if the terminfo DB supports it on every host you connect to.
2. Catting Binary Files Into the Terminal¶
You run cat /var/log/journal/somefile or cat coredump.bin and your terminal turns into a mess. Characters are garbled, the prompt disappears, keys do not echo. What happened: binary bytes included escape sequences that changed terminal modes, character sets, or window titles.
Fix: Type reset or stty sane blindly (your keystrokes may not echo). Prevent it by never using cat on files you have not verified:
file suspicious_file # check type first
less suspicious_file # handles binary safely
hexdump -C suspicious_file | head
Configure your shell to warn: alias cat='cat -v' makes non-printing characters visible but changes output for scripts. Better: just build the habit of checking with file first.
3. Not Resetting the Terminal After a Program Crashes in Raw Mode¶
Programs like vim, top, and less switch the terminal to raw mode (no line buffering, no echo processing) on startup and restore it on clean exit. If the program crashes or is killed with SIGKILL (kill -9), the restore never happens. Now your terminal has no echo, no line editing, and special characters do not work.
Fix: stty sane restores sane defaults. reset does a full reinitialize. Add a trap to scripts that change terminal modes:
4. Forgetting That SSH Escape Sequences Require a Newline First¶
You are stuck in a frozen SSH session and type ~. to disconnect. Nothing happens. You type it again. Still nothing. You close the terminal window, losing your tmux session context and any unsaved work.
Fix: SSH escape sequences only work immediately after a newline. Press Enter first, then ~.. The full sequence is: Enter, ~, .. If you are inside a nested SSH session, use ~~. to reach the outer SSH.
Common escapes: ~. disconnect, ~? help, ~^Z suspend SSH, ~# list forwarded connections.
5. Running Interactive Programs in Non-Interactive Shells¶
You write a script that runs ssh host 'vim /etc/config' or launches less inside a cron job. The program either fails immediately, hangs, or produces garbage. Interactive programs need a TTY. Cron, scripts piped through SSH, and most CI/CD runners do not allocate one.
Fix: For SSH, use -t or -tt to force PTY allocation: ssh -tt host 'sudo vim /etc/config'. In scripts, check for a TTY before running interactive commands:
For CI/CD, avoid interactive programs entirely. Use sed, envsubst, or template engines instead.
6. Ignoring Locale Mismatches Over SSH¶
Your local machine is en_US.UTF-8. The remote server is C or POSIX or has no locale installed. SSH forwards LC_* and LANG variables by default. Programs on the remote side try to use your forwarded locale, fail to find it, and emit warnings. Worse, tools like sort, grep, and awk behave differently under different locales. Character class matching ([a-z]) produces different results in C vs en_US.UTF-8.
Fix: Either install matching locales on the remote host, or stop forwarding locale in SSH config:
# On the server: /etc/ssh/sshd_config
# Comment out: AcceptEnv LANG LC_*
# Or on the client: ~/.ssh/config
Host problematic-server
SendEnv -LANG -LC_*
For scripts that depend on consistent behavior, set LC_ALL=C explicitly.
7. Losing Output Because stdout and stderr Go to Different Places¶
You run ./deploy.sh > deploy.log and see error messages on your terminal but not in the log. The errors went to stderr (fd 2) which was not redirected. You think the deploy succeeded because the log looks clean.
Fix: Always redirect both streams when capturing output:
./deploy.sh > deploy.log 2>&1 # both to same file
./deploy.sh > out.log 2> err.log # separate files
./deploy.sh |& tee deploy.log # both to file AND screen (bash)
Remember: 2>&1 > file is NOT the same as > file 2>&1. Redirect order is left to right. The first sends stderr to the original stdout (terminal), then redirects stdout to the file. The second redirects stdout to the file, then sends stderr to where stdout now points (the file).
8. Treating Ctrl+C as a Reliable Kill Mechanism¶
You press Ctrl+C and assume the process died. Some processes trap SIGINT and ignore it (databases doing graceful shutdown, programs with custom signal handlers). Others catch it and start a cleanup that takes minutes. Some interpret it as "cancel current operation" but keep running.
Fix: Ctrl+C sends SIGINT. If the process does not die, try Ctrl+\ which sends SIGQUIT (usually not trapped, often produces a core dump). As a last resort, find the PID and send SIGKILL:
In scripts, trap signals properly and always have a cleanup path. Do not assume SIGINT means instant death.
9. Pasting Multi-Line Text Into a Terminal Without Bracketed Paste¶
You copy a code snippet from a web page and paste it into your terminal. If the snippet contains newlines, each line executes as a separate command immediately. If the snippet contains a hidden rm -rf / or a curl-pipe-bash command, it runs before you can read it.
Fix: Modern terminals support bracketed paste mode, which wraps pasted text in escape sequences so the shell treats it as literal text, not commands. Bash 5+ and zsh enable this by default. Verify:
If your terminal or shell does not support it, paste into a text editor first, inspect, then paste into the terminal. Never paste from untrusted sources directly.
10. Not Understanding isatty() Behavior Changes¶
Your script produces colorful, nicely formatted output when you run it manually. When you pipe it to a file or another command, the colors disappear, progress bars vanish, and the output format changes. Or the reverse: a program outputs ANSI codes into a log file, making it unreadable.
Fix: Programs call isatty() to check if their output goes to a terminal. They change behavior accordingly — adding colors and progress bars for interactive use, stripping them for pipes. To force terminal behavior in a pipe:
# Force colors even when piped
script -qc 'ls --color=always' /dev/null | less -R
unbuffer command | grep pattern # from expect package
# Force NO colors to a terminal
ls --color=never
TERM=dumb command
NO_COLOR=1 command # emerging standard
11. tmux and Screen Eating Key Bindings¶
You press Ctrl+B in tmux to go back one character in bash. Nothing happens — tmux consumed it as its prefix key. You press Ctrl+A in screen to go to the beginning of the line. Nothing — screen ate it. Your muscle memory is broken and you waste time relearning keybindings inside multiplexers.
Fix: Know the prefix keys and how to send them literally. In tmux, Ctrl+B then Ctrl+B sends a literal Ctrl+B. In screen, Ctrl+A then A sends a literal Ctrl+A. Or rebind the prefix:
# tmux.conf: use Ctrl+A instead of Ctrl+B
set -g prefix C-a
unbind C-b
bind C-a send-prefix
# Or use Ctrl+Space (does not conflict with common readline bindings)
set -g prefix C-Space
12. Assuming Terminal Width Is 80 Columns¶
Your script formats output for 80 columns. On a narrow terminal, lines wrap mid-word and tables become unreadable. On a wide terminal, output is cramped in the left quarter of the screen. Worse, some programs (like docker ps) truncate output based on detected terminal width, hiding information.
Fix: Query the actual terminal width:
For docker and similar tools, use --no-trunc or --format to avoid width-based truncation. When generating formatted output in scripts, always check the available width and adapt, or use a fixed-width format that works everywhere.
13. SSH Agent Forwarding Across Multiple Hops¶
You SSH from A to B to C with agent forwarding (-A). On host B, your SSH agent socket is exposed. Anyone with root on B can use your forwarded agent to authenticate as you to any host your key can reach. Agent forwarding is a convenience feature with real security implications.
Fix: Use ProxyJump instead of agent forwarding through untrusted intermediaries:
# ~/.ssh/config
Host target
ProxyJump bastion
# This tunnels through bastion without exposing your agent there
ssh target
If you must use agent forwarding, limit it to specific trusted hosts and use ssh-add -c to require confirmation for each key use.
14. Ignoring SIGHUP When Closing Terminals¶
You start a long-running process in an SSH session, close the terminal, and the process dies. The kernel sent SIGHUP to all processes in the terminal's process group. You thought background processes would survive — they do not unless explicitly protected.
Fix: Use one of these before starting long-running commands:
nohup long_command & # ignore SIGHUP
disown %1 # remove from shell's job table
tmux new -d -s bg 'long_command' # full session persistence
The best practice is to always use tmux or screen for anything that should survive disconnection. nohup is a patch; tmux is a solution.