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.
The thirty-second diagnosis
Section titled “The thirty-second diagnosis”Run this. It prints a 64-step gradient in 24-bit colour:
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:
████████████████████████████████████████████████████████████████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:
████████████████████████████████████████████████████████████████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.
The short version
Section titled “The short version”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:
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:
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:
echo $TERMtput colorstput 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.
Layer two: terminfo cannot count past 256
Section titled “Layer two: terminfo cannot count past 256”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
RGBboolean, which marks an entry as direct-colour capable. It went into ncurses in August 2017. - The
*-directentries —xterm-direct,tmux-directand friends — which setcolors#16777216outright.
Which brings us to macOS, where this becomes funny:
infocmp -V # ncurses 6.0.20150808TERM=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:
brew install ncursesTERMINFO=/opt/homebrew/opt/ncurses/share/terminfo \ /opt/homebrew/opt/ncurses/bin/tput -T xterm-direct colors # 16777216None 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.
Layer three: enabling truecolor in tmux
Section titled “Layer three: enabling truecolor in 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.
default-terminal is already right
Section titled “default-terminal is already right”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:
tmux -f /dev/null start-server \; show -g default-terminal \; kill-server# default-terminal tmux-256colorIf 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.
tmux does not read COLORTERM
Section titled “tmux does not read COLORTERM”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:
strings "$(command -v tmux)" | grep -c COLORTERM # 0The 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.
Test it before you commit to it
Section titled “Test it before you commit to it”tmux takes a features list on the command line, which beats editing a config to find out:
tmux -T RGB new-sessionRun the gradient inside that. Smooth means the only thing missing was the declaration.
The line to keep
Section titled “The line to keep”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:
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 functionalE558: Terminal entry not found in terminfoSwap 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:
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.
So what is COLORTERM for?
Section titled “So what is COLORTERM for?”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:
strings "$(command -v bat)" | grep -c COLORTERMRun 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.
Truecolor in Vim: termguicolors
Section titled “Truecolor in Vim: termguicolors”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:
" ~/.vimrcif has('termguicolors') let &t_8f = "\<Esc>[38;2;%lu;%lu;%lum" let &t_8b = "\<Esc>[48;2;%lu;%lu;%lum" set termguicolorsendift_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.
The fix, per layer
Section titled “The fix, per layer”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.