tmux
Keep shells alive across SSH drops, split a terminal into panes, copy text without a mouse and script sessions from Bash.
On this page
Cheatsheet#
Every key below follows the prefix, C-b by default. C-b c means press Ctrl-b, release, then c.
| Task | Command or key |
|---|---|
| Start a named session | tmux new -s work |
| Attach, creating if missing | tmux new -A -s work |
| List sessions | tmux ls |
| Attach and kick other clients off | tmux attach -d -t work |
| Detach | C-b d |
| New window | C-b c |
| Next, previous, numbered window | C-b n, C-b p, C-b 0-9 |
| Rename window | C-b , |
| Split pane below, right | C-b ", C-b % |
| Move between panes | C-b + arrow, or C-b o |
| Zoom a pane to full size and back | C-b z |
| Kill the current pane | C-b x |
| Interactive session and window picker | C-b s, C-b w |
| Enter copy mode, scroll back | C-b [, then PgUp or C-u |
| Paste the last copy | C-b ] |
| Command prompt | C-b : |
| Show every binding | C-b ? or tmux list-keys |
| Reload config | tmux source-file ~/.tmux.conf |
| Run a command in a detached session | tmux new -d -s job 'make test 2>&1 | tee test.log' |
| Kill one session, or everything | tmux kill-session -t work, tmux kill-server |
Behaviour below is tmux 3.4 or later. Options and commands that arrived in a specific release are marked. Reference: tmux(1).
The server, sessions, windows and panes#
The first tmux command starts a server process bound to a socket in /tmp/tmux-$UID/default. The server owns everything: sessions, the windows in them, the panes in each window and the shells running in those panes. The terminal you type in is a client attached to one session. Closing the terminal, losing the SSH connection or pressing C-b d drops the client; the server and every process under it keep running.
| Object | Holds | Addressed as |
|---|---|---|
| Server | Sessions | Socket: -L name or -S /path |
| Session | Windows, a working directory, environment | -t work |
| Window | Panes in a layout; one is current | -t work:2 or -t work:editor |
| Pane | One pseudo-terminal running one process | -t work:2.1 |
Most commands take -t target. A bare number or name means a session, session:window selects a window and session:window.pane a pane. Window names match on prefix, so -t work:ed finds editor if nothing else starts with ed. tmux display -p '#S:#I.#P' prints the current target from inside a pane.
tmux new -s work -n editor -c ~/projects # session "work", first window "editor", cwd set
tmux new -d -s build -x 200 -y 50 # detached; size matters for tools that read the terminal width
tmux ls # sessions, window counts, attached or not
tmux attach -t work # a second client on the same session mirrors it
tmux attach -d -t work # detach the others first: fixes a window sized to a small screen
tmux switch-client -t build # move this client to another session without detaching
tmux rename-session -t work api
tmux kill-session -t build # kills every process in itPanes size to the smallest attached client. When someone attaches from a phone, everyone’s window shrinks to that size; attach -d or C-b D (choose a client to detach) fixes it. Since tmux 3.1 window-size defaults to latest, which sizes to the most recently active client rather than the smallest, so this bites less than it used to.
Windows and panes#
tmux new-window -n logs 'journalctl -fu my-app' # a window running one command closes when that command exits
tmux split-window -h -c '#{pane_current_path}' # right of the current pane, same directory
tmux split-window -v -l 10 'htop' # below, ten lines high
tmux select-pane -t :.+ # next pane in the current window
tmux resize-pane -Z # toggle zoom
tmux resize-pane -D 5 # five lines shorter
tmux select-layout even-horizontal # also even-vertical, main-horizontal, main-vertical, tiled
tmux break-pane # current pane becomes its own window
tmux join-pane -s logs -t editor # window "logs" becomes a pane in "editor"
tmux swap-pane -U # exchange with the pane above
tmux swap-window -s 3 -t 1
tmux respawn-pane -k # kill what is running and restart the pane's commandA pane’s process is normally the login shell, which is why exiting the shell closes the pane. A pane created with a command instead runs only that command; remain-on-exit on keeps a dead pane visible so its last output can be read, and respawn-pane restarts it.
| Key | Effect |
|---|---|
C-b Space | Cycle through layouts |
C-b {, C-b } | Move the pane up or down in the layout |
C-b q | Show pane numbers; press one to jump there |
C-b ! | Break the pane into a window |
C-b & | Kill the window and every pane in it, after confirmation |
C-b . | Move the window to another index |
C-b f | Search window names and contents |
C-b t | Show a clock; any key clears it |
Prefix and key tables#
Keys live in tables. prefix holds the bindings that follow C-b; root holds keys that work without a prefix; copy-mode and copy-mode-vi hold the copy-mode keys. bind-key (alias bind) adds to prefix unless -T names another table, and -n is shorthand for -T root. -r makes a key repeatable within repeat-time (500 ms), so C-b followed by several arrow presses keeps resizing.
tmux list-keys -T prefix # everything under the prefix
tmux list-keys -T copy-mode-vi | grep -i copy
tmux bind -r H resize-pane -L 5 # repeatable
tmux bind -n M-Left select-pane -L # Alt-Left, no prefix
tmux unbind C-b; tmux set -g prefix C-a; tmux bind C-a send-prefix # C-a as prefix, C-a C-a sends a literal C-asend-prefix matters because the prefix key itself never reaches the application. With C-a as prefix, Emacs and Bash’s beginning-of-line need C-a C-a. Many people keep C-b for that reason, or use C-Space.
Copy mode#
Copy mode freezes the pane and lets you move around the scrollback (history-limit lines, 2000 by default) with the keys of mode-keys: emacs by default, vi if VISUAL or EDITOR contains vi, or whatever the config sets. Text selected in copy mode goes into a tmux paste buffer, and C-b ] pastes the most recent one into the current pane as if typed.
| Action | vi keys | emacs keys |
|---|---|---|
| Enter copy mode | C-b [ | C-b [ |
| Move | h j k l, w b, 0 $, g G | arrows, M-f M-b, C-a C-e, M-< M-> |
| Page | C-u C-d, C-b C-f | M-v C-v |
| Search | / forward, ? backward, n N | C-s, C-r |
| Start selection | Space or v (after binding, see below) | C-Space |
| Rectangle toggle | C-v (after binding) | R |
| Copy and leave | Enter | M-w |
| Leave without copying | q or Escape | Escape |
Commands in copy mode are sent with send-keys -X, which is how bindings are written and how scripts drive it:
set -g mode-keys vi
bind -T copy-mode-vi v send-keys -X begin-selection
bind -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind -T copy-mode-vi y send-keys -X copy-selection-and-cancel
bind -T copy-mode-vi Escape send-keys -X cancel
bind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel # mouse selections go to copy-command tootmux copy-mode -e # -e: leave copy mode when scrolled back to the bottom
tmux list-buffers # every buffer, newest first
tmux show-buffer # print the newest to stdout
tmux save-buffer ~/out.txt # write it to a file
tmux load-buffer ~/in.txt # file into a buffer
tmux set-buffer "$text" # string into a buffer
tmux paste-buffer -t work:1 # paste into a specific pane
tmux capture-pane -p -S - # print the whole scrollback of the current pane, no copy mode neededcapture-pane -p -S - -E - -t work:logs > pane.txt is the reliable way to get a pane’s history into a file from a script; -J joins wrapped lines.
Clipboard#
Copying in tmux fills a tmux buffer, not the system clipboard. Two mechanisms bridge the gap.
OSC 52 lets tmux hand the text to the terminal emulator, which sets the clipboard itself. It works through SSH because it travels as an escape sequence inside the session, with no X forwarding needed. It needs three things: set-clipboard at on or external (the default is external, which lets tmux set the clipboard but stops programs inside tmux from doing so), the terminal’s terminfo entry to carry the Ms capability, and the terminal to allow OSC 52. Most modern terminals (foot, kitty, WezTerm, Alacritty, iTerm2, Windows Terminal) do; GNOME Terminal and other VTE-based ones did not until VTE 0.76. Add the capability with terminal-features when it is missing:
set -s set-clipboard on # also lets programs inside (Neovim, for example) use OSC 52
set -as terminal-features ',xterm-256color:clipboard' # tmux 3.2+; the name is the outer TERM, not tmux's owncopy-command (tmux 3.2+) pipes every copy through an external program instead. It is the right choice when the terminal does not support OSC 52 or when copying must reach a specific clipboard:
set -s copy-command 'wl-copy' # Wayland; xclip -selection clipboard for X11; pbcopy on macOS
set -s set-clipboard off # avoid double-copying on terminals that also handle OSC 52copy-command runs on the machine where the tmux server lives. On a remote server it copies into the remote’s clipboard, which is useless; use OSC 52 there.
.tmux.conf essentials#
tmux reads ~/.tmux.conf then $XDG_CONFIG_HOME/tmux/tmux.conf (3.1+) when the server starts. Later changes need tmux source-file or a new server. set-option (alias set) takes -g for the global value, -s for server options, -w for window options and -a to append to a string option. A wrong option name reports an error on load; tmux show-options -g prints the effective values.
# Terminal: tmux's own TERM, and what the outer terminal can do
set -g default-terminal 'tmux-256color' # falls back to screen-256color if terminfo lacks it
set -as terminal-features ',xterm-256color:RGB' # 24-bit colour on this outer TERM (3.2+); older: terminal-overrides ',*:Tc'
set -s escape-time 10 # ms to wait after Escape; default 500 makes Vim feel broken
set -s extended-keys on # pass Ctrl-Shift and similar combinations through (3.2+)
set -g focus-events on # Vim and Neovim see FocusGained/FocusLost
set -g allow-passthrough on # apps may send escape sequences straight to the outer terminal (3.3+): images, OSC 52 from inside
# Behaviour
set -g history-limit 50000
set -g mouse on # click panes, drag to resize, wheel to scroll
set -g base-index 1 # windows count from 1; 0 is far from the other keys
set -gw pane-base-index 1
set -g renumber-windows on # close window 2 of 4 and the rest shift down
set -g mode-keys vi
set -g status-keys emacs # readline-style editing at the : prompt
set -g display-time 2000 # message duration, ms
set -g set-titles on
set -g set-titles-string '#S:#W #{pane_title}'
# Keys
unbind C-b
set -g prefix C-Space
bind C-Space send-prefix
bind r source-file ~/.tmux.conf \; display 'reloaded'
bind | split-window -h -c '#{pane_current_path}'
bind - split-window -v -c '#{pane_current_path}'
bind c new-window -c '#{pane_current_path}'
bind -r h select-pane -L
bind -r j select-pane -D
bind -r k select-pane -U
bind -r l select-pane -R
# Status line
set -g status-interval 5
set -g status-left '#[bold]#S #[default]'
set -g status-right '#(uptime | sed "s/.*load average: //") %H:%M'#{...} is a format: pane_current_path, session_name, window_index, pane_pid, client_width and hundreds more, listed under FORMATS in the manual. #(cmd) runs a shell command every status-interval seconds and inserts its first line. Formats support conditionals, #{?client_prefix,PREFIX,}, which many status lines use to show when the prefix is pending.
default-terminal must be a tmux-* or screen-* value; setting it to xterm-256color breaks key handling and colours in ways that look unrelated. If infocmp tmux-256color fails, the terminfo database is too old; on Fedora and RHEL it is in ncurses-term.
Scripting tmux from Bash#
Every tmux command works from a script, whether or not a client is attached. Commands find the server through the socket, so a script and an interactive session on the same machine and user share state. The building blocks are new-session -d to start work without attaching, send-keys to type into a pane, has-session to make scripts idempotent and capture-pane or pipe-pane to read output.
#!/usr/bin/env bash
set -euo pipefail
session=dev
root=$HOME/projects/my-app
if ! tmux has-session -t "=$session" 2>/dev/null; then # "=" forces an exact name match
tmux new-session -d -s "$session" -n editor -c "$root"
tmux send-keys -t "$session:editor" 'vim .' Enter
tmux new-window -t "$session" -n server -c "$root"
tmux send-keys -t "$session:server" 'make run' Enter
tmux split-window -t "$session:server" -v -l 15 -c "$root"
tmux send-keys -t "$session:server.1" 'tail -f log/dev.log' Enter
tmux select-window -t "$session:editor"
fi
if [[ -n ${TMUX:-} ]]; then
tmux switch-client -t "$session" # already inside tmux: nesting an attach is refused
else
exec tmux attach -t "$session"
fisend-keys types characters; Enter is a key name, not a string, so send-keys 'ls' Enter runs the command while send-keys 'ls Enter' types the letters. Use -l to send a string literally when it contains something that looks like a key name. Typing into a pane races with the shell’s start-up: on a slow host the shell has not printed its prompt yet and the keys are still delivered, which is fine, but a pane started with a command that is still initialising (a REPL, a database shell) may drop them. Wait on the prompt or use wait-for:
tmux new-session -d -s job "make test; tmux wait-for -S job-done" # signal the channel when the command ends
tmux wait-for job-done # block until it does
tmux capture-pane -p -t job -S - > test-output.txtFor output that must be recorded from the start, pipe-pane copies everything the pane prints to a command:
tmux pipe-pane -t work:server -o 'cat >> ~/server.log' # -o toggles: the same command again stops itrun-shell executes a command from a binding or script and shows its output in the pane; display-message -p prints a format to stdout, which is the way to read state from a script. if-shell -F '#{==:#{session_name},dev}' 'cmd' 'other' branches on a format without spawning a shell.
tmux display -p -t work:server '#{pane_pid}' # PID of the shell in the pane
tmux list-panes -a -F '#{session_name}:#{window_index}.#{pane_index} #{pane_current_command}'
tmux list-windows -t work -F '#{window_index} #{window_name} #{window_activity}'
tmux set-environment -t work API_URL https://api.example.com # new panes in the session inherit it
tmux new-session -d -s api -e API_TOKEN="$API_TOKEN" # 3.2+: environment for the whole sessionRemote persistence with SSH#
The standard pattern is a tmux server on the remote host. Work happens inside it, and reconnecting is ssh host -t 'tmux new -A -s main', which attaches when the session exists and creates it otherwise. -t forces a pseudo-terminal, which SSH does not allocate when it is given a command. A ssh_config entry keeps that short:
Host build
HostName build.example.com
RequestTTY yes
RemoteCommand tmux new -A -s mainDetaching (C-b d) or a dropped connection leaves everything running. tmux ls on the remote shows it as (attached) until the SSH connection has timed out, which can take minutes; attach -d takes over immediately.
The environment a session captures at creation is what its panes inherit. SSH_AUTH_SOCK changes on every new SSH connection, so a shell inside a session that has been alive for days points at a socket that no longer exists and git push fails with Permission denied (publickey). update-environment lists variables tmux copies from the attaching client into the session’s environment (SSH_AUTH_SOCK, DISPLAY, SSH_CONNECTION and a few others by default), but running shells keep their old copy. Refresh it in the shell:
eval "$(tmux show-environment -s SSH_AUTH_SOCK)" # re-export the value tmux has nowA stable symlink avoids this entirely: point SSH_AUTH_SOCK at ~/.ssh/agent.sock inside tmux and have ~/.ssh/rc relink it on every connection.
# ~/.ssh/rc on the remote host: runs on each connection, in sh
if [ -S "$SSH_AUTH_SOCK" ] && [ "$SSH_AUTH_SOCK" != "$HOME/.ssh/agent.sock" ]; then
ln -sf "$SSH_AUTH_SOCK" "$HOME/.ssh/agent.sock"
fiThen [[ -n ${TMUX:-} ]] && export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock in the shell rc makes every pane, old or new, use the current agent.
A tmux server does not survive a reboot. On a personal server a user unit brings one back:
# ~/.config/systemd/user/tmux.service
[Unit]
Description=tmux server
[Service]
Type=forking
ExecStart=/usr/bin/tmux new-session -d -s main
ExecStop=/usr/bin/tmux kill-server
[Install]
WantedBy=default.targetsystemctl --user enable --now tmux with loginctl enable-linger "$USER" so the user manager starts at boot; see systemd. Sessions and their layout are not saved; plugins such as tmux-resurrect restore layouts and cwd, not the running processes.
Nested sessions#
Running tmux inside an SSH session inside a local tmux is common and works; the inner and outer servers just both want C-b. Press C-b C-b to send one prefix through to the inner tmux, or give the inner server a different prefix in its config. A cleaner approach binds a key to toggle the outer prefix off:
# outer .tmux.conf
bind -T root F12 set prefix None \; set key-table off \; refresh-client -S
bind -T off F12 set -u prefix \; set -u key-table \; refresh-client -Skey-table off sends every key straight to the inner session until F12 is pressed again. The TMUX variable being set is how tmux refuses attach from inside a session; TMUX= tmux attach works around it deliberately, and is how the nested case happens by accident when an SSH client passes the variable through.
Oneliners#
# Attach to "main" or create it; the everyday entry point
tmux new -A -s main
# Run a long job detached, log it, and notify when done
tmux new -d -s backup 'rsync -a /data/ backup.example.com:/data/ 2>&1 | tee ~/backup.log; notify-send "backup finished"'
# Send the same command to every pane in the current window (toggle with the same command)
tmux setw synchronize-panes on
# Type a command into every pane of the window without sync mode
for p in $(tmux list-panes -F '#{pane_id}'); do tmux send-keys -t "$p" 'sudo dnf update -y' Enter; done
# One pane per host, tiled, each running an SSH session
for h in web-1 web-2 db-1; do tmux split-window -c ~ "ssh $h"; tmux select-layout tiled; done; tmux kill-pane -t 0
# Kill every session except the current one
tmux kill-session -a
# Kill sessions with no attached client
tmux ls -F '#{session_name} #{session_attached}' | awk '$2==0{print $1}' | xargs -rn1 tmux kill-session -t
# Save the whole scrollback of the current pane to a file
tmux capture-pane -p -J -S - > ~/pane-$(date +%s).txt
# Search scrollback for a pattern and print matching lines with pane context
tmux capture-pane -p -S - | grep -n 'ERROR'
# Move the current pane into a window of its own, then bring it back later
tmux break-pane -n scratch; tmux join-pane -s scratch -t work:1
# Rename the current window after the running command
tmux rename-window "$(tmux display -p '#{pane_current_command}')"
# Toggle the status line to reclaim a row
tmux set status
# Show the effective value of one option and where it was set
tmux show-options -g escape-time; tmux show-options -gs terminal-features
# Which TERM and colour capabilities does tmux believe the outer terminal has
tmux display -p '#{client_termname} #{client_termfeatures}'
# Resize the current window to the largest attached client
tmux resize-window -A
# Open a popup shell over the current pane (3.2+); Escape or exit closes it
tmux display-popup -E -w 80% -h 80%
# Watch a pane's output in another pane
tmux pipe-pane -o 'cat >> /tmp/watch.log'; tmux split-window 'tail -f /tmp/watch.log'
# Use a private server for a throwaway environment, isolated from the default socket
tmux -L scratch new -s test
# Clear history of the current pane
tmux clear-history
# List every pane with its PID, so `kill` can target the process not the pane
tmux list-panes -a -F '#{session_name}:#{window_index}.#{pane_index} #{pane_pid} #{pane_current_command}'Scripts#
Start a project workspace idempotently and attach or switch to it.
#!/usr/bin/env bash
# usage: workspace <name> <directory>
set -euo pipefail
name=${1:?session name}
dir=${2:?directory}
[[ -d $dir ]] || { printf 'no such directory: %s\n' "$dir" >&2; exit 1; }
if ! tmux has-session -t "=$name" 2>/dev/null; then
tmux new-session -d -s "$name" -n shell -c "$dir"
tmux new-window -t "$name" -n edit -c "$dir" "${EDITOR:-vim}"
tmux new-window -t "$name" -n git -c "$dir"
tmux send-keys -t "$name:git" 'git status' Enter
tmux select-window -t "$name:edit"
fi
if [[ -n ${TMUX:-} ]]; then tmux switch-client -t "$name"; else exec tmux attach -t "$name"; fiRun a command on many hosts in parallel, each in its own pane, and leave the panes open for inspection.
#!/usr/bin/env bash
# usage: fanout "<command>" host1 host2 ...
set -euo pipefail
cmd=${1:?command}; shift
(( $# )) || { echo 'no hosts' >&2; exit 2; }
session="fanout-$$"
tmux new-session -d -s "$session" -x 220 -y 60 "ssh -o ConnectTimeout=10 $1 $(printf '%q' "$cmd"); echo '[done: $1]'; exec \$SHELL"
shift
for host in "$@"; do
tmux split-window -t "$session" "ssh -o ConnectTimeout=10 $host $(printf '%q' "$cmd"); echo '[done: $host]'; exec \$SHELL"
tmux select-layout -t "$session" tiled
done
tmux setw -t "$session" synchronize-panes off
exec tmux attach -t "$session"Report every session, its windows and what each pane is running, for finding forgotten work before a reboot.
#!/usr/bin/env bash
set -euo pipefail
tmux ls -F '#{session_name}' 2>/dev/null | while IFS= read -r s; do
attached=$(tmux display -p -t "$s" '#{session_attached}')
printf '%s (%s clients)\n' "$s" "$attached"
tmux list-panes -s -t "$s" -F ' #{window_index}:#{window_name}.#{pane_index} #{pane_current_command} #{pane_current_path}'
doneTroubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
open terminal failed: missing or unsuitable terminal | The outer TERM has no terminfo entry on this host | infocmp "$TERM"; set TERM=xterm-256color before tmux, or install the terminal’s terminfo |
| Colours wrong or only 8 colours inside tmux | default-terminal unset or a non-256-colour value; tmux-256color terminfo missing | tmux info | grep -E 'colors|Tc|RGB'; infocmp tmux-256color; install ncurses-term |
| True colour washed out in Vim or Neovim | Outer terminal not declared RGB-capable | set -as terminal-features ',xterm-256color:RGB' and restart the server |
sessions should be nested with care, unset $TMUX to force | tmux attach from inside tmux | tmux switch-client -t name, or TMUX= tmux attach when nesting is intended |
| Escape takes half a second in Vim | escape-time default of 500 ms | set -s escape-time 10 |
| Home, End, Ctrl-arrows do nothing | default-terminal set to xterm-*, or the application ignores tmux-* | Use tmux-256color or screen-256color; cat -v and press the key to see what arrives |
| Copy does not reach the system clipboard | OSC 52 not enabled or unsupported, or copying on a remote server with copy-command | Check tmux display -p '#{client_termfeatures}' for clipboard; enable Ms via terminal-features; test with printf '\033]52;c;%s\a' "$(printf hi | base64)" outside tmux |
| Mouse scroll enters copy mode but selection is unusable | mouse on intercepts the drag | Hold Shift while selecting to bypass tmux, or use copy mode keys |
| Window shrinks and shows dots on the right | A smaller client is attached | tmux attach -d, or tmux resize-window -A |
git push fails with Permission denied (publickey) in an old session | Stale SSH_AUTH_SOCK | eval "$(tmux show-environment -s SSH_AUTH_SOCK)" or the symlink pattern above |
error connecting to /tmp/tmux-1000/default (No such file or directory) | No server running, or /tmp cleaned by systemd-tmpfiles while the server lives on | tmux ls confirms; if the process exists, kill -USR1 <tmux pid> recreates the socket |
Keys typed by send-keys arrive as literal Enter text | The key name was inside the quoted string | Pass key names as separate arguments: send-keys 'cmd' Enter |
| Config change has no effect | Server still running with old options | tmux source-file ~/.tmux.conf, or tmux kill-server and start again |
Pane closes immediately after new-window 'cmd' | The command exited, and a pane lives only as long as its command | Append ; exec $SHELL, or setw remain-on-exit on to inspect the output |
tmux info prints every terminfo capability tmux resolved for the outer terminal, which settles most colour and key arguments. tmux -vv new writes tmux-server-*.log and tmux-client-*.log in the current directory with every byte in and out.
Further reading#
- tmux(1): the complete option, command, key and format reference
- tmux wiki: FAQ, clipboard, and terminal-specific advice from upstream
- tmux CHANGES: which release introduced an option
- OpenSSH ssh_config(5):
RemoteCommandandRequestTTYfor the attach-on-connect pattern