Skip to content

Truecolor in tmux, SSH and Vim: Fixing TERM

Your terminal can draw sixteen million colours. Your theme was chosen over the course of an evening you will not be getting back. And then you open tmux, or SSH into a server, and the careful gradient in your status line turns into four shades of mud.

Nothing errors. Nothing is logged. Colour is negotiated across four layers — the terminal, a database of terminal descriptions curated since the 1980s, your multiplexer, and whatever is at the far end of the SSH connection — and every one of them is entitled to quietly hand you something worse than you asked for. The result is a vague sense that colours are off, and no obvious thread to pull.

Here is the thread.

Run this. It prints a 64-step gradient in 24-bit colour:

Terminal window
awk 'BEGIN{ for (i=0;i<64;i++) { r=int(255-i*3.2); g=int(40+i*2.6); b=int(120+i*1.8)
printf "\033[38;2;%d;%d;%dm█", r, g, b } printf "\033[0m\n" }'

If truecolour is reaching your screen, you get a smooth wash:

Terminal window
████████████████████████████████████████████████████████████████

If something in the stack is downgrading you, you get bands. This is the same command, captured from a tmux session that has not been told its terminal can do better — 64 distinct colours collapsed into 11:

Terminal window
████████████████████████████████████████████████████████████████

Banding means something between awk and your eyes is rounding every colour to the nearest of 256. The rest of this guide is about finding which of the four layers is doing it.

If you are here for the fix and not the tour, it is usually one of three lines.

Banded inside tmux, smooth outside it — tell tmux what the outer terminal can do, using that terminal’s own TERM:

~/.tmux.conf
set -as terminal-features ",xterm-256color:RGB"

Broken over SSH, with unknown terminal type or a screen that will not clear — copy your terminfo entry to the remote host:

Terminal window
infocmp -x | ssh yourhost 'mkdir -p ~/.terminfo && tic -x -'

Everything colourful except Vim — set termguicolors, plus the two escape sequences in the Vim section.

If none of those is it, the table at the end maps each symptom to its layer, and the sections below explain why each layer does what it does.

Layer one: TERM is a database key, not a description

Section titled “Layer one: TERM is a database key, not a description”

TERM does not tell anything what your terminal can do. It is a lookup key into terminfo, a database of compiled entries describing terminal types, most of which have been dead for forty years. Your entry is a claim made about a type of terminal, not a conversation with the one you are using.

Ask what your entry claims:

Terminal window
echo $TERM
tput colors

tput reads terminfo and reports the colors number. What comes back is rarely what people expect:

TERM tput colors What that means
xterm-256color 256 The common, sane default
tmux-256color 256 What tmux sets inside its panes
screen-256color 256 The older multiplexer entry, still everywhere
xterm-color 8 Has “color” in the name. Offers eight of them
xterm 8 Eight, despite every xterm shipped since 1999
vt100 -1 No colour capability at all
dumb -1 Honest, at least

xterm-color is the trap worth knowing: it is an ancient entry, it sounds like the colourful option, and it caps you at eight. If a login script or a terminal preference pane has set it, that alone is your bug.

Here is the structural problem. For most of terminfo’s life, colors#256 was the largest number anyone needed, so there was no way for an entry to say “sixteen million”. Two mechanisms were added later:

  • The RGB boolean, which marks an entry as direct-colour capable. It went into ncurses in August 2017.
  • The *-direct entries — xterm-direct, tmux-direct and friends — which set colors#16777216 outright.

Which brings us to macOS, where this becomes funny:

Terminal window
infocmp -V # ncurses 6.0.20150808
TERM=xterm-direct tput colors # tput: unknown terminal "xterm-direct"

Apple ships the August 2015 snapshot of ncurses. The RGB capability was added two years after it. Scan the entire database — all 1,670 entries — and not one of them carries RGB, because the software that would have written it did not exist yet. There are no -direct entries either. On a stock macOS, no value of TERM can declare truecolour, because the database has no word for it.

Homebrew’s ncurses is current and does:

Terminal window
brew install ncurses
TERMINFO=/opt/homebrew/opt/ncurses/share/terminfo \
/opt/homebrew/opt/ncurses/bin/tput -T xterm-direct colors # 16777216

None of this stops your terminal from rendering 24-bit colour. The escape sequence works whether or not the database admits it — which is exactly why the failure is so slippery. The database matters to the programs that consult it before deciding what to emit: ncurses applications, Vim, and above all tmux.

Two lines appear in roughly every tmux config on the internet:

set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",xterm-256color:Tc"

They are mostly cargo. Here is what is actually true on tmux 3.5a.

Since tmux 3.3, the build picks it: the first of tmux-256color, tmux, screen-256color, screen available on the build machine. Check yours with no config at all:

Terminal window
tmux -f /dev/null start-server \; show -g default-terminal \; kill-server
# default-terminal tmux-256color

If that prints tmux-256color, the first line is a no-op. It earns its place only when your tmux was built somewhere the entry was missing — which does still happen on older distributions, and is why the line spread in the first place.

This is the one that wastes afternoons. Your terminal almost certainly exports COLORTERM=truecolor, and a great deal of advice implies tmux picks it up. It does not:

Terminal window
strings "$(command -v tmux)" | grep -c COLORTERM # 0

The string is not in the binary, and the behaviour matches — setting COLORTERM=truecolor before starting tmux changes nothing about the bytes it emits. COLORTERM is a convention between terminals and applications, and tmux is not participating.

What tmux actually does is read RGB or Tc from terminfo, and — since 3.2 — detect a handful of terminals from their DA and DSR query responses. The list is short. In the 3.5a binary it is iTerm2, mintty, XTerm, tmux and screen. kitty, Alacritty, WezTerm and Ghostty are not on it; they depend on their terminfo entries instead, and alacritty and wezterm as shipped by ncurses 6.5 carry no RGB flag. So “it works in iTerm2 but not in Alacritty” is not superstition, and it is not a bug in either.

tmux takes a features list on the command line, which beats editing a config to find out:

Terminal window
tmux -T RGB new-session

Run the gradient inside that. Smooth means the only thing missing was the declaration.

All four of these work — verified by capturing what tmux emits, not by reading forums:

In ~/.tmux.conf Works Notes
set -as terminal-features ",xterm-256color:RGB" Yes The documented modern form
set -ga terminal-overrides ",xterm-256color:RGB" Yes Standard capability, older mechanism
set -ga terminal-overrides ",xterm-256color:Tc" Yes tmux’s own extension, still honoured
set -ga terminal-overrides ",*:Tc" Yes Claims it for every terminal. Don’t

Use the first. terminal-features exists precisely because terminfo databases go stale, which is the problem you have; terminal-overrides patches individual capabilities and is the blunter tool. The wildcard version is how people end up with mangled colours over SSH from a terminal that genuinely cannot do it: you have told tmux that every terminal in the world supports RGB, including the one on the eight-colour serial console.

Match the pattern to the TERM of the terminal tmux is running in, not the one it sets inside its panes:

~/.tmux.conf
set -as terminal-features ",xterm-256color:RGB"
set -as terminal-features ",xterm-kitty:RGB"

There is more worth fixing in a tmux config than this — the tmux tutorial covers the rest, and the cheat sheet covers the keys. If you are weighing the alternative, Zellij does read COLORTERM, which is one fewer thing to configure and one more thing to be surprised by.

Layer four: SSH and missing terminfo entries

Section titled “Layer four: SSH and missing terminfo entries”

SSH sends your TERM to the remote host. The remote host looks it up in its terminfo database. If it isn’t there, you get a shell that cannot clear its own screen and a htop that resembles modern art — or, more often, a flat refusal. These are the real messages, from a host with no xterm-kitty entry, from clear and nano, htop, less and vim respectively:

'xterm-kitty': unknown terminal type.
Error opening terminal: xterm-kitty.
WARNING: terminal is not fully functional
E558: Terminal entry not found in terminfo

Swap in xterm-ghostty, alacritty or wezterm as appropriate; the cause is identical.

This is the well-known cost of terminals that set their own TERM. It is worse than it looks, because the entries are missing from more than just old servers — xterm-kitty and xterm-ghostty are absent from ncurses 6.5, the current release. Both ship with their applications rather than with ncurses, so any host without the application installed has never heard of them.

The fix is to post your entry ahead of you:

Terminal window
infocmp -x | ssh yourhost 'mkdir -p ~/.terminfo && tic -x -'

infocmp -x dumps your current entry with extended capabilities intact; tic -x - compiles it into ~/.terminfo on the remote side, where ncurses looks before the system database. No root, no package manager, done once per host. kitty automates exactly this with kitten ssh; Alacritty leaves it to you. That difference is covered properly in Alacritty vs kitty, along with why both projects are right to do it.

If you are doing this more than twice, it belongs in your SSH config — and your local entry belongs in your dotfiles along with everything else.

It is a convention: terminals set it, and applications may read it. Which ones do is not documented anywhere central — but it is greppable, one binary at a time:

Terminal window
strings "$(command -v bat)" | grep -c COLORTERM

Run across the usual suspects, in the versions current as this was written, that splits the field:

Reads COLORTERM Does not
bat, delta, zellij tmux, vim, fzf, rg, eza, less, git, htop, btop, starship

A zero is strong evidence and not quite proof — a program could assemble the string at runtime, and another build of the same tool can answer differently — but it puts the burden of proof somewhere useful, and it is a check you can repeat on whatever you actually have installed.

Keep COLORTERM=truecolor exported either way, because the tools that read it are ones you use (bat and delta among them). Just stop expecting it to reach the layers that ignore it.

Vim needs telling twice. It renders 24-bit colour only with termguicolors, and inside a TERM whose terminfo lacks the RGB capabilities — tmux-256color, for one — it also needs to be handed the escape sequences directly:

" ~/.vimrc
if has('termguicolors')
let &t_8f = "\<Esc>[38;2;%lu;%lu;%lum"
let &t_8b = "\<Esc>[48;2;%lu;%lu;%lum"
set termguicolors
endif

t_8f and t_8b are the foreground and background truecolour sequences. Vim fills them in by itself only for terminals it recognises; everywhere else they stay empty, termguicolors has nothing to emit, and you get 256 colours with no warning — the same silent downgrade as everything else in this article, one layer further down.

Work from the outside in. Each layer can only downgrade what the one above it passed along, so the first no is the one to fix.

Symptom Layer Fix
Bands everywhere, even outside tmux Terminal Check TERM isn’t xterm-color or xterm
tput colors says 8 where you expected 256 terminfo Set TERM=xterm-256color, or install current ncurses
Smooth outside tmux, banded inside tmux set -as terminal-features ",xterm-256color:RGB"
Fine locally, broken over SSH Remote infocmp -x | ssh host 'tic -x -'
Everything colourful except Vim Vim termguicolors plus t_8f / t_8b
htop in monochrome on one box only Remote Missing terminfo entry — same tic fix

The gradient one-liner at the top works at every level: run it in the terminal, run it in tmux, run it over SSH, run it inside tmux over SSH. The first place the bands appear is the layer that is lying to you, and now you know what each of them wants to hear.

Once the colours survive the trip, they are worth spending on: a prompt that earns the space and a terminal that renders it properly.