Andrey's Blog

tmux: the parts worth knowing

Everything in tmux follows from one fact: it is a client/server program. The tmux command you type is a thin client. A server process holds the sessions, and every pane in every session is a pseudo-terminal that the server owns. Detach, reattach, survive a closed terminal, share a session between two terminals — all of it is just clients coming and going while the server keeps the terminals alive.

The hierarchy is session → window → pane. A session is a project or a context. A window is a full-screen tab inside it. A pane is a split inside a window. Default prefix is Ctrl+b; every shortcut below means: press prefix, release, then the key.

Cheat sheet

The handful that covers a normal day.

tmux new -s dev -c ~/projects/myapp     # new named session in a directory
tmux ls                                 # list sessions
tmux a -t dev                           # attach
tmux a -d -t dev                        # attach, kick other clients off
tmux kill-session -t dev
tmux kill-server                        # everything
KeysAction
prefix ddetach — everything keeps running
prefix ssession picker
prefix c / prefix ,new window / rename it
prefix 1 … prefix 9jump to window
prefix llast window (edit ↔ test bounce)
prefix % / prefix "split side by side / top-bottom
prefix ←↑→↓move between panes
prefix zzoom pane fullscreen (toggle)
prefix Spacecycle layouts — fixes a lopsided split
prefix xkill pane
prefix [copy mode / scrollback
prefix :command prompt
prefix ?list every binding

If you learn only two keys beyond navigation, make them prefix z and prefix l.

Sessions

One session per project or context, not per task. Then prefix s becomes a project switcher.

From the shell, tmux new -s name -c dir creates and attaches. From inside tmux, do not run tmux new -s other — it nests a second client inside the first, and the prefix has to be pressed twice to reach the inner one. Use the command prompt instead:

prefix :
new-session -s infra                     # create and switch to it
new-session -d -s infra                  # create in the background, stay put
new-session -s infra -c ~/projects/k8s   # with a starting directory
KeysAction
prefix ddetach
prefix ssession tree — → expands a session to show its windows
prefix $rename session
prefix ( / prefix )previous / next session
prefix Llast session (capital L, toggles)

switch-client -t name from the command prompt targets by name; mostly for scripts.

Windows

KeysAction
prefix ccreate
prefix ,rename
prefix winteractive picker with previews
prefix n / prefix pnext / previous
prefix 1 … prefix 9jump by index
prefix llast window (toggles)
prefix &kill (asks to confirm)

Rename windows. Three windows called bash give you no navigation cues. Direct numbers cover most switching; prefix l is the fast bounce between two. Closing the last window ends the session; exit in the shell closes a window too.

Panes

KeysAction
prefix %split side by side (vertical divider)
prefix "split top/bottom (horizontal divider)
prefix ←↑→↓navigate
prefix ocycle panes
prefix qshow pane numbers, type one to jump
prefix zzoom fullscreen (toggle)
prefix xkill pane
prefix !break pane out into its own window
prefix { / prefix }swap pane position
prefix Spacecycle preset layouts
prefix Alt+1 … Alt+5select a layout directly

The naming is backwards from expectation: % is tmux’s “vertical split” because the divider is vertical.

Resizing

KeysEffect
prefix Ctrl+←↑→↓resize by 1 cell
prefix Alt+←↑→↓resize by 5 cells

Press prefix once, then hold the modifier and tap the arrow repeatedly. Both bindings are declared -r (repeatable), so tmux keeps the prefix active for repeat-time (500ms default); pause longer and you drop out. Direction is relative to the current pane — you push the wall nearest that direction. Left/right resizing between vertically stacked panes does nothing, silently, because no vertical border exists.

Precise sizing via prefix ::

resize-pane -D 10        # down 10 rows
resize-pane -L 20        # left 20 columns
resize-pane -y 15        # set height to exactly 15 rows
resize-pane -x 80        # set width to exactly 80 columns

In practice prefix z and prefix Space replace resizing most of the time: zoom is non-destructive, and Space snaps a broken layout back to something sane.

Copy mode

prefix [ enters, prefix ] pastes, q exits. With mode-keys vi you get h j k l, w/b, Ctrl+u/Ctrl+d, / to search, g/G for top and bottom — a read-only Vim buffer over your scrollback. Default vi-mode selection is Space to start and Enter to copy, not v/y; the config below fixes that.

Config

Where it lives, and the trap

Preferred location since tmux 3.1 (2020) is ~/.config/tmux/tmux.conf. The search order is:

  1. /etc/tmux.conf (system-wide, loaded separately)
  2. ~/.tmux.conf
  3. $XDG_CONFIG_HOME/tmux/tmux.conf
  4. ~/.config/tmux/tmux.conf

tmux loads the first user config it finds and stops. It does not merge. If ~/.tmux.conf exists at all — even empty — the XDG one is silently ignored. Move it, don’t copy it:

mkdir -p ~/.config/tmux
mv ~/.tmux.conf ~/.config/tmux/tmux.conf
tmux display -p '#{config_files}'      # verify which file is live
tmux -V

The config

set -g mouse on
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -g mode-keys vi
set -g history-limit 50000
set -sg escape-time 10
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",*256col*:Tc,foot*:Tc"
set -g extended-keys on
set -as terminal-features 'foot*:extkeys'
bind r source-file ~/.config/tmux/tmux.conf \; display "config reloaded"

# splits and new windows keep the current directory
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"

# vim-style selection in copy mode
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi y send -X copy-selection-and-cancel

Line by line

mouse on — click to select panes, drag borders to resize, scrollwheel for scrollback. Without it the wheel sends arrow keys to the shell and you scroll through history instead. Tradeoff: mouse selection grabs tmux’s pane content, not the terminal’s; hold Shift while dragging for native selection.

base-index 1 + pane-base-index 1 — default numbering starts at 0, which puts window 0 under the 0 key at the far right of the number row. Starting at 1 aligns numbering with the physical keys. setw is set-window-option; pane indexing is a window-level setting.

renumber-windows on — closing window 2 of 1 2 3 otherwise leaves 1 3, and gaps accumulate.

history-limit 50000 — scrollback per pane. The default is 2000, which one verbose build blows through. Only applies to panes created after the setting loads.

escape-time 10 — the important one. tmux cannot distinguish a bare ESC from the ESC that starts an escape sequence (arrow keys are ESC [ A), so it waits. The default 500ms means a half-second pause every time you leave insert mode in Neovim. 10ms still catches real sequences from a local keyboard and feels instant. -s makes it server-level.

default-terminal "tmux-256color" — what $TERM becomes inside panes. The older screen-256color claims italics aren’t supported, so Neovim silently renders comments without them.

terminal-overrides ... :Tc — truecolor. Without it, Neovim colorschemes get quantized to 256 colors and look muddy.

extended-keys on + terminal-features ':extkeys' — needed for Shift+Enter and other modified keys. Requires tmux 3.5+; on 3.2–3.4 use set -s extended-keys always. Change the foot* glob to match your terminal (xterm-kitty*, wezterm*, xterm-ghostty*); -a appends, so several can be listed. Test with sed -n l and press Shift+Enter: \033[13;2u means it works end to end.

bind r — \; is an escaped semicolon; tmux chains commands with ;, but the config parser would eat a bare one. Caveat: source-file applies new settings but does not undo removed ones. A deleted binding lives in the running server until tmux kill-server. Server-scope options (-s) also need a full restart, not a reload.

#{pane_current_path} — by default a new split inherits the session’s start directory, so splitting three levels deep dumps you back at $HOME. This makes splits open where you are. % and " keep working alongside | and -.

Optional

Vim-style pane navigation shadows the default l = last-window, so decide which you use more:

bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R

Ctrl+a as prefix, inherited from GNU screen. The send-prefix line lets Ctrl+a Ctrl+a send a literal Ctrl+a, preserving readline’s jump-to-start-of-line:

unbind C-b
set -g prefix C-a
bind C-a send-prefix

TPM defaults to ~/.tmux/plugins/. To keep it under XDG, set the path and clone TPM to the matching location — decide before installing plugins, since moving them later means re-cloning:

set -g @plugin 'tmux-plugins/tpm'
run '~/.config/tmux/plugins/tpm/tpm'

Scripted sessions

#!/usr/bin/env bash
tmux new-session  -d -s dev -n editor -c ~/projects/myapp
tmux new-window      -t dev  -n server -c ~/projects/myapp
tmux new-window      -t dev  -n logs   -c ~/projects/myapp
tmux select-window   -t dev:editor
tmux attach          -t dev

-d creates detached so later commands aren’t fighting an attached client; -n names the window at creation; -c sets its directory; -t dev:editor targets by name (dev:2 by index also works). Drop it in ~/.local/bin/dev-session, chmod +x, and one command rebuilds the layout.

You can launch commands directly — tmux new-window -t dev -n server 'npm run dev' — but the window closes when the command exits, so a crashed process makes a window silently vanish. A plain shell is more forgiving.

Flag placement

tmux [server-flags] command [command-flags]

Server flags (-L, -S, -f, -u, -2) go before the command. Command flags (-s, -c, -n, -d) go after.

tmux -L work new -s app -c ~/projects/app    # correct
tmux new -L work -s app                      # wrong

The wrong form fails with no server running on /tmp/tmux-1000/default — note it says default, not work. The -L work was parsed as a flag to new-session, which has no -L, so the socket name never got set.

-L is not sticky. Every later command needs it too, or it looks at default and finds nothing:

tmux -L work ls
tmux -L work a -t app
tmux -L work kill-session -t app
tmux -L work kill-server       # also removes the socket file; ls reporting "no server" afterwards is expected

An alias helps: alias twork='tmux -L work'.

Troubleshooting