For daily drivers who never learned the model

Detach.
Everything else keeps running.

One server owns the work; terminals are just views that come and go.

tmux 3.7b tested checked 2026-08-22 tmux 3.7c now available · not yet re-verified macOS + Linux single file · works offline prefix playground simulator

1 · Mental model

The SSH connection drops. The laptop sleeps mid-compile. The terminal freezes and you kill it. None of that has to touch the work. tmux moves your shells out of the terminal and into a process that does not care about your connection — so when a view comes back, everything is exactly where you left it.

A window manager, not a terminal

tmux is not Terminal.app, iTerm2, or Alacritty, and it does not replace them. You run it inside a terminal. The terminal still draws every glyph on the screen. What tmux decides is which glyphs: it owns your programs and their layouts, composites them into one character grid, and hands that grid to the terminal to render. It is a window manager for terminals — the same relationship a tiling window manager has to your monitor.

Four nouns do the work, nested one inside the next:

  • Server — one long-lived process that owns everything: the sessions, the programs inside them, the scrollback.
  • Session — a workspace. Holds windows.
  • Window — one full-screen view inside a session. The tab-like unit. Holds panes.
  • Pane — one pseudo-terminal running one program. The thing you actually type into.

One noun sits outside the nest: the client — a terminal plus a connection to the server. Part II gives each of these its own section. For now you only need the nesting and one rule: processes live in panes, panes live in windows, windows live in sessions, sessions live in the server, and clients live nowhere near any of it.

One server, many clients

Every tmux command you type — tmux new, tmux ls, tmux attach — is a client process. It finds the server through a Unix socket in /tmp/tmux-$(id -u)/ and talks to it. Most commands finish and exit at once; an attached client stays connected until you detach or close its terminal. The server starts itself the first time it is needed, keeps running after the last client has gone, and exits only when its last session ends. You will watch that entire lifecycle happen in sixty seconds in §3.

That split is the product. Detaching is not pausing and not closing — it is a client hanging up. The session keeps running, headless, inside the server. Attach again from any terminal: the same one, a new one, an SSH session from another machine. The session cannot tell the difference and does not care. One server, many views, coming and going.

tmux object tree: server contains sessions, sessions contain windows, windows contain panes, panes hold processes, clients attach from outside tmux server one process · starts on demand · outlives every client socket: /tmp/tmux-501/default session: alpha tmux new -s alpha window 0 — "edit" the tab-like unit pane %0 zsh └─ vim notes.md one pseudo-terminal, one process (§7) pane %1 zsh └─ tail -f app.log a split of the same window, seen at once window 1 — "logs" another window, same session, its own panes …more sessions would sit beside alpha, one box each (§5) attach attach client A Terminal.app, local a view, nothing more client B ssh from laptop another view, same session
The map. One server process owns sessions; each session owns windows; each window tiles panes; each pane is one pseudo-terminal holding one process. Clients sit outside the server — a terminal plus a socket connection — and attach to sessions from below. Detaching removes a client and nothing else. Later sections refer back to this as the map (§1).

Three misconceptions to drop now

Misconception: "tmux is a terminal." It looks like one — it draws a status bar, it tiles panes, it feels like an app. It is a window manager that runs inside your terminal. The test takes two seconds: you launched it from a shell prompt, in a window another program drew. Two programs are involved — your terminal renders; the tmux server decides what gets rendered. That is why tmux cannot improve your font, and why it behaves identically locally and over SSH.
Misconception: "Detaching pauses my programs." The view disappears, so it feels like a pause. Nothing pauses. Detach removes a client; the programs keep running inside the server. The honest version cuts the other way, too: even without tmux a background job can outlive its terminal — as an orphan you can never reach again (§2 has the transcript). tmux's promise is not "your processes won't die." It is: the shell, its working directory, its environment, your editor mid-buffer, and the scrollback all stay alive together, attached to a server you can re-enter.
Misconception: "Panes are tabs." Tabs switch — one thing visible at a time. Panes multiply — several things visible at once, inside a single window. The tab-like unit in tmux is the window; panes are simultaneous splits of a window. And the pane is the unit that holds a process: close the pane and the process running in it ends with it (§7). Windows are views; panes are where processes live.

Sources: tmux(1) — OpenBSD manual, overview · tmux GitHub wiki

2 · Prerequisite floor

You have used a terminal for years without needing to know what one is. tmux ends that arrangement: it wedges itself into the exact seam between your terminal and your programs, and every confusing thing it does happens at that seam. Three concepts put you back on solid ground — who draws, what $TERM promises, and what a hangup signal really does.

Terminal, shell, multiplexer — who does what

Three kinds of program share your one terminal window. The table is the section; the rest of the guide assumes it.

ProgramDrawsOwns as child processesWhat ends it
Terminal emulator
Terminal.app · iTerm2 · Alacritty
The window, every glyph, the font, mouse and clipboard. The only program here that renders anything. Your shell — the one it launched at startup. Closing the window hangs up its pseudo-terminal; the shell receives SIGHUP.
Shell
zsh
Nothing. It prints characters and lets the terminal worry about pixels. Every command you run — foreground jobs, cmd & background jobs, editors. Exiting the shell (by choice or by SIGHUP) orphans its children.
tmux server Only its own UI: the status line and pane borders. Everything else in the grid is your programs' output. Every shell in every pane. Your terminal is not their parent — the server is. Not terminal death. It exits when its last session ends (§4).

$TERM: the capability contract

$TERM is not your terminal's name. It is the name programs use to look up capabilities — how many colors, how to move the cursor, whether the clipboard is reachable — in the terminfo database. Programs never ask your terminal what it can do; they ask $TERM.

tmux sits in the middle of every byte, so it does the only sensible thing: each pane gets a brand-new terminal with its own identity. From the outside, your terminal reports something like xterm-256color — this is how tmux itself describes a real attached client:

$ tmux list-clients
/dev/ttys031: alpha [80x24 xterm-256color] (attached,focused,UTF-8)

Inside a pane, $TERM is different — tmux-256color on a stock install (some configurations say screen-256color). Real pane contents, fresh server, one command typed in:

$ tmux -L first -f /dev/null new-session -d -s try
$ tmux -L first send-keys -t try 'echo "inside: $TERM"' Enter
$ tmux -L first capture-pane -p -t try
…
❯ echo "inside: $TERM"
inside: tmux-256color

The stock default it comes from, straight off the server:

$ tmux -L first show-options -s default-terminal
default-terminal tmux-256color

Carry two consequences forward. Programs inside tmux are talking to tmux, not to your terminal — one terminal, two contracts, tmux bridging them. And every "the colors are wrong inside tmux" symptom is this contract breaking somewhere along that chain; §10 repairs it properly.

SIGHUP: what actually happens when the terminal dies

A terminal connection is a pseudo-terminal. Close the window, drop the SSH, let the laptop sleep mid-session: the pty hangs up, and the kernel delivers SIGHUP — the hangup signal — to the processes attached to it, delivered by process group. The shell owns the connection, so it gets the signal and exits. That much is standard Unix and always true.

What happens to the shell's children is the interesting part, and the folklore answer — "closing the terminal kills your jobs" — did not survive contact with the machine this guide was tested on. Real transcript: a bare interactive zsh on a pty, one backgrounded job, then the terminal closes:

### close the terminal (pty master)
zsh exited rc=1
### ps after terminal closed:
  PID STAT COMMAND
99451 SN   sleep 300

The shell died. The backgrounded sleep survived it — reparented, orphaned, and unreachable: no terminal, no shell, no way back in. It had to be killed by hand.

The same experiment inside tmux, from the same research run — terminal closed while a client was attached, then the session deliberately killed:

### sleep after terminal closed (session detached, process alive):
  PID STAT COMMAND
96174 SN   sleep 300
### kill-session -t alpha
### ps after kill-session (shell and sleep gone):
  PID STAT COMMAND
(no rows)
The honest pitch Without tmux, a process may well survive your terminal dying — as an orphan you can never reach again. What dies is the session: the shell, its working directory, its environment, the editor mid-buffer, the scrollback, and any route back to what survived. Inside tmux, the terminal closing just detaches a client; the shell and its state keep running in the server, and kill-session is what ends them — on purpose, visibly. Survival is not the product. Survival plus re-entry is the product.

Self-assessment

Five things you should be able to say out loud. If any comes out wrong, re-read the sub-section named after it before continuing — Part II leans on all five.

  • I can point at my screen and name which of the three programs draws the glyph I am reading. (re-read: Terminal, shell, multiplexer)
  • I can say whose child my interactive shell is right now, and whose child it becomes inside tmux. (re-read: the table above — §4 shows it with ps)
  • I can explain why $TERM changes when I enter tmux, and name the stock value inside a pane. (re-read: $TERM)
  • I can describe SIGHUP: who sends it, who receives it, and when. (re-read: SIGHUP)
  • I can state what tmux guarantees about a detached session — and why "your processes would die otherwise" is not the argument. (re-read: The honest pitch)

Sources: tmux(1) — TERMINALS section, default-terminal · signal(7) — SIGHUP · transcripts captured on macOS, tmux 3.7b, 2026-08-22

3 · Install & first session

Installing tmux takes thirty seconds. The section is really about the sixty seconds after: one full lap of the server lifecycle — create, detach, observe from outside, re-enter, end it — so that "the server owns the work" becomes something you have watched happen, not something you were told.

Install

macOS

brew install tmux

Homebrew tracks releases closely. This is the right default.

Linux — Debian/Ubuntu

sudo apt install tmux

Distribution packages lag. Check the version below before blaming tmux for a missing feature.

Building from source is out of scope for this guide. If you need it, the releases page ships tarballs and the repository README lists the dependencies — that pointer is as far as we go.

Verify the version

$ command -v tmux
/usr/local/bin/tmux
$ tmux -V
tmux 3.7b

Captured on the machine this guide was verified on: tmux 3.7b, macOS 15.6 (Darwin 24.6.0). Everything ahead was checked against the 3.7 series; where your output should differ, the guide says so.

The 60-second loop

Five commands. Together they are the entire mental model of §1, performed once. Run them in any terminal.

1

Create a session

tmux new -s try

Expect: your terminal becomes a tmux session — a status line along the bottom, the session name try at the far left, one window, one pane, running your shell. If no server was running, one just started for you, silently, in the background. There is no message. §4 shows you where it lives.

2

Detach

Press Ctrl+b, release, then d.

Expect: your plain shell returns, status line gone. The session did not close — it is running headless inside the server. You just removed your client, nothing else.

3

Observe from outside

tmux ls

Expect:

try: 1 windows (created Sun Aug 23 00:48:49 2026)

Real output, captured on Lab 1's throwaway server below; your timestamp differs. This line is the whole guide in miniature — the session outlived your leaving it.

4

Come back

tmux attach

Expect: the session returns — same window, same pane, scrollback intact. Nothing re-ran and nothing was restored; it never stopped. With more than one session, name the target: tmux attach -t try. Detach again before the last step.

5

End it — on purpose

kill-server is the nuclear option It ends every session in the server — every window, every pane, every running program. If you already keep tmux sessions (check tmux ls first), run Lab 1 below instead: the same loop on a private named server that cannot touch anything you care about.
tmux kill-server

Expect: no output; the command succeeds. The receipt for what just ended, captured verbatim on Lab 1's throwaway server:

$ tmux -L first kill-server
$ tmux -L first ls
no server running on /private/tmp/tmux-501/first
$ echo "exit=$?"
exit=1

On the default server, the last path component reads default; the 501 is the user id — id -u on this machine. Killing the only session ends the server the same way: a server exits when its last session ends, by the exit-empty default (§4). "No server running" here is not a failure; it is the correct description of an empty world.

Lab 1 · First session
5 minutes · local · throwaway

The same loop on a private named server: every command below takes -L first, which means "use the socket named first instead of the default one." Your default server — if one is running — never sees any of this, and the final kill-server can only ever hit this throwaway. §4 makes named servers precise.

1. Create and attach:

tmux -L first new -s try

Expect: a fresh session named try fills the terminal. You are now a client. Detach with Ctrl+b, d.

2. From your plain shell, prove it survived:

$ tmux -L first ls
try: 1 windows (created Sun Aug 23 00:48:49 2026)

Expect: exactly that line, your timestamp in place of mine.

3. Re-enter, then leave again:

tmux -L first attach

Expect: the session as you left it — scrollback intact, cursor where it was. Detach once more.

4. End the server and read the receipt:

$ tmux -L first kill-server
$ tmux -L first ls
no server running on /private/tmp/tmux-501/first
$ echo "exit=$?"
exit=1

Expect: the no-server error and exit=1 — the server exited when its work ended, and saying so is not an error condition.

Checkpoint:

  • tmux -L first ls listed the session after you detached — survival, demonstrated
  • You re-attached and found the scrollback intact — re-entry, demonstrated
  • kill-server produced the no-server error — and you can say why that is the correct end state

Teardown: kill-server already ended everything, so there is nothing to clean up. Confirm with pgrep -fl 'tmux -L first' — it prints nothing.

Sources: Homebrew — tmux formula · tmux releases · tmux GitHub wiki — installing

4 · The server

You have never started the server, and you never will. It starts itself the first time a session is created, runs with no terminal of its own, and exits the moment its work runs out. Nearly every tmux mystery — "where did my session go?", "why is there no server running?", "what did that command just talk to?" — dissolves once you can point at the process. So point at it.

What is actually running

Start a throwaway session the way every transcript in this guide was captured, then ask the process table:

$ tmux -L lab -f /dev/null new-session -d -s try
$ ps aux | grep '[t]mux -L lab'
you              68328   0.0  0.0 34562324   2172   ??  Ss   11:09PM   0:00.02 tmux -L lab -f /dev/null new-session -d -s try

That line is the server. Two details reward a second look. The state column reads Ss — sleeping, and a session leader with no controlling terminal. It is attached to nothing, and it does not need to be. And the command line still reads like the client that created it: on macOS the server keeps the ps identity of the command that spawned it, rather than relabeling itself. A server started by a plain tmux attach years ago shows up as just tmux — the one this guide was written next to has been doing exactly that since July. One line in ps, one server, however it is labeled.

That launch line's flags, once: -L lab names the socket (next sub-section), and -f /dev/null replaces the config file with nothing — stock tmux, zero user configuration. Every research transcript in this guide runs that way, so what you read is what an out-of-the-box tmux does. Your everyday server, with your config loaded, may differ where this guide says so.

The socket: where clients find the server

Each server owns exactly one Unix socket — a special file that client processes connect to. They live in a per-user directory with mode 700; no other account can reach your servers:

$ ls -la /tmp/tmux-$(id -u)/
total 0
drwx------@   5 you       wheel   160 Aug 22 23:09 .
drwxrwxrwt  278 root       wheel   8896 Aug 22 23:09 ..
srwxrwx---@   1 you       wheel     0 Jul  9 18:28 default
srw-rw----@   1 you       wheel     0 Jul 13 13:43 gt-01778c
srw-rw----@   1 you       wheel     0 Aug 22 23:09 lab

Real directory, real servers: default is this machine's everyday server, alive since July 9; gt-01778c a second one; lab the throwaway from the transcript above. One file per server, one server per file.

A command with no -L talks to the socket named default. That is the entire meaning of "the default server." -L name picks any other name and gets you a separate server that shares nothing with the default one except this directory. (-S path does the same job with a full filesystem path; you may go your whole life without it. The 501 in the path is this machine's user id — id -u.)

Drift: a dead server leaves its socket file behind. On this machine (macOS, tmux 3.7b) the socket file is not unlinked when the server exits. After kill-server, lab still sat in the listing above — and every throwaway server in this guide's research left the same corpse. A later server on the same -L name reuses the path without complaint. Consequence: the file's existence tells you nothing. Whether a server is alive is answered by asking it, not by listing the directory — the exit-code table below is the reliable test.

Lifetime: born on demand, dies with its work

Three rules, each verified live:

  • It starts when a session is created. The first new-session quietly spawns a server if none exists. No message, no output — you watched this in §3. Commands that do not create — ls, attach — never start one; they just report no server running. (There is also an explicit tmux start-server; you will rarely have a reason.)
  • It outlives every client. Detach, close the terminal, drop the SSH, let the laptop sleep. The server does not notice. It has no terminal to lose.
  • It exits when its last session ends — by default. This is the exit-empty server option, shipped on and visible in the server options dump: exit-empty on.

That third rule has an escape hatch, and pulling it produces one of the strangest sights in tmux — a live server with nothing inside it:

$ tmux -L t6 -f /dev/null new-session -d -s one
$ tmux -L t6 set-option -s exit-empty off
$ tmux -L t6 show-options -s exit-empty
exit-empty off
$ tmux -L t6 kill-session -t one
$ tmux -L t6 ls; echo "exit=$?"
exit=0
$ tmux -L t6 new-session -d -s two
$ tmux -L t6 ls
two: 1 windows (created Sun Aug 23 01:09:33 2026)

Fresh capture on a throwaway server, 2026-08-23. The server survived the death of its only session, and ls — with zero sessions to list — printed nothing and exited 0. Restoring exit-empty on and killing two ended the server with the familiar no server running, exit 1.

Put the three states of a socket name side by side, all observed:

Statels saysExit status
No server (never started, or last session ended with exit-empty on)no server running on /private/tmp/tmux-501/NAME1
Server running, one or more sessionsone line per session0
Server running, zero sessions (exit-empty off)nothing at all0

Silence with exit 0 is the server saying "I am here, and I have nothing." The error with exit 1 says "there is no work to reconnect to." Neither is a failure condition to fix — which is why §3 drilled the receipt. "No server running" after a kill-server you typed is the correct answer; after a reboot it is also the correct answer, for a sadder reason:

Misconception: "tmux runs as a daemon." The server feels daemon-like — faceless, always there, surviving your logout. It is a plain process owned by your user, sitting in ps like any other, parented to whatever launched it. Nothing registers it with launchd or systemd; nothing restarts it. A reboot clears /tmp, and your sessions are gone — not paused, gone. This is the honest limit of the durability promise, and §16 starts there. If you truly need server-managed sessions, that is a deliberate piece of infrastructure you build (the spec's capstone points the way), not something tmux does silently.

Named servers, and why you want them

One machine can run any number of servers, one per socket name, in total ignorance of each other. Lab 2 below runs two side by side and proves the blindness from inside. What named servers are for:

  • Throwaways. Every lab in this guide lives on a -L socket so its final kill-server cannot touch anything you care about. Adopt the habit for anything unfamiliar.
  • Isolated configuration. Server options — exit-empty, escape-time, default-terminal — are per-server. A second server with -f /dev/null is a clean-room tmux for testing config without reloading your everyday one.
  • Hard project separation. A socket per context (work, personal, the flaky experiment) means one kill-server can only ever be as destructive as its own world.

The cost is remembering which world you are in: tmux -L work attach is a mouthful, and a bare tmux attach will never find it. §12 scripts that friction away.

Lab 2 · Two servers
5 minutes · local · throwaway

Two servers, side by side, on lab and lab2 sockets. Your default server never appears in this lab — no command below omits -L.

1. Start both:

tmux -L lab -f /dev/null new-session -d -s alpha
tmux -L lab2 -f /dev/null new-session -d -s beta

Expect: no output, twice. Two servers are now running; neither knows the other exists.

2. Ask each what it holds:

$ tmux -L lab ls
alpha: 1 windows (created …)
$ tmux -L lab2 ls
beta: 1 windows (created …)

Expect: each ls sees exactly its own session — the captured pair from this guide's fresh run read alpha: 1 windows (created Sun Aug 23 01:12:58 2026) on one socket and beta: 1 windows (created Sun Aug 23 01:12:58 2026) on the other, identical timestamps, zero shared state. Your timestamps differ; the blindness does not.

3. Prove the blindness from inside — lab2 reaches for lab's session:

$ tmux -L lab2 new-window -t alpha -n x
can't find window: alpha
$ echo "exit=$?"
exit=1

Expect: that refusal, exit 1. To lab2, alpha is not hidden or busy — it does not exist. Separate socket, separate universe.

4. See both in the flesh:

ls -la /tmp/tmux-$(id -u)/ | grep -E 'lab2?$'
ps aux | grep -E '[t]mux -L (lab|lab2)'

Expect: two fresh socket files in the directory, and two server processes in ps — each still wearing the command line that spawned it, per the drift note above.

5. End both worlds:

$ tmux -L lab kill-server
$ tmux -L lab2 kill-server
$ tmux -L lab ls
no server running on /private/tmp/tmux-501/lab

Expect: each ls after its kill-server prints the no-server error. Two independent exits — one kill-server never reached across.

Checkpoint:

  • Each server's ls listed only its own session — no flag ever shared state
  • The cross-server command failed with can't find window — you can say why that is blindness, not a permissions error
  • Both kill-server calls were surgical, and you know what the lingering socket files in the directory mean (nothing)

Teardown: kill-server on both sockets ended everything; pgrep -fl 'tmux -L lab' prints nothing. The socket files linger — that is the macOS drift, not a leftover server.

Sources: tmux(1) — CLIENTS AND SERVERS; start-server; exit-empty · transcripts: research captures 2026-08-22 (ps, socket directory, kill receipts) and fresh -L t6/-L t6b captures 2026-08-23 (exit-empty states, isolation), all on throwaway servers · socket naming detail

5 · Sessions

You already know the session is the unit that survives — §1 made that point before you installed anything. What the mental model does not prepare you for is how cheap sessions are. The daily-driver habit this section breaks is one giant session stretched across every project. The feature is a workspace per context — named, listed, and switchable in a single chord without ever detaching. A deliberately fast pass over the basics first: verify them, do not skip them. The checkpoint cards at the end prove you own them.

Chord notation, from here on. §3 wrote the detach chord out in full. From now on it is compressed: Prefix d means press Ctrl+b, release it, then press d. The prefix behaves like a shift key for commands — always release it, never hold it.

The fast pass

Create two sessions on a throwaway server, list them, rename one, kill one:

$ tmux -L lab -f /dev/null new-session -d -s alpha
$ tmux -L lab new-session -d -s beta
$ tmux -L lab ls
alpha: 1 windows (created Sat Aug 22 23:09:37 2026)
beta: 1 windows (created Sat Aug 22 23:09:37 2026)
$ tmux -L lab rename-session -t beta work
$ tmux -L lab ls
alpha: 1 windows (created Sat Aug 22 23:09:37 2026)
work: 1 windows (created Sat Aug 22 23:09:37 2026)
$ tmux -L lab kill-session -t alpha
$ tmux -L lab ls
work: 1 windows (created Sat Aug 22 23:09:37 2026)

Four details, half a second each. -d creates the session detached — it exists without any client. Rename keeps the creation timestamp: new name, same object. kill-session ends the session and everything in it — the deliberate, visible ending from §2's honest pitch. And sessions created without -s get numbers for names, in sequence:

$ tmux -L t6 -f /dev/null new-session -d -s alpha
$ tmux -L t6 new-session -d
$ tmux -L t6 new-session -d
$ tmux -L t6 ls
1: 1 windows (created Sun Aug 23 01:10:20 2026)
2: 1 windows (created Sun Aug 23 01:10:20 2026)
alpha: 1 windows (created Sun Aug 23 01:10:20 2026)

Numbered sessions sort before named ones. Fine for throwaways; name anything you intend to keep, because attach -t 2 is a mystery six months later and attach -t project is not.

Detaching, attaching, stealing

Detach with Prefix d. Come back with tmux attach — or tmux attach -t name once more than one session exists, because a plain attach takes the most recently used one. Naming the target is the habit worth building now.

Attach while another client — a colleague, or another terminal of yours — is already attached, and by default you both stay: two clients mirroring one session. Real transcript, two clients of different sizes:

$ tmux -L t6 list-clients
/dev/ttys013: alpha [100x30 xterm-256color] (attached,focused,UTF-8)
/dev/ttys014: alpha [80x24 xterm-256color] (attached,focused,UTF-8)
$ tmux -L t6 ls
alpha: 1 windows (created Sun Aug 23 01:10:43 2026) (attached)
beta: 1 windows (created Sun Aug 23 01:10:43 2026)

Two things to notice. The (attached) marker in ls appears the moment any client is attached — a one-glance "is anyone in there?" from outside. And both clients see the same window, the same pane, the same cursor; a shared session is a shared screen, not a shared folder. When you want the terminal to yourself, attach with -d and steal it:

$ tmux -L t6 attach -d -t alpha
$ tmux -L t6 list-clients
/dev/ttys018: alpha [80x24 xterm-256color] (attached,focused,UTF-8)

One row remains: the newcomer detached both earlier clients on its way in. Their terminals dropped to their shells; the session itself never changed. -d is "detach others as I arrive" — the polite-heist version of detach-client, which §8 covers properly.

Switching without detaching

The underused move. Prefix s opens an interactive tree of sessions — type to filter, Enter to land. Prefix ( and Prefix ) step to the previous and next session outright. Same client, same terminal, different workspace, no detach. Verified by driving the actual chords into a real attached client:

### client attached to alpha
$ tmux -L t6 list-clients -F '#{client_tty} #{client_session}'
/dev/ttys018 alpha
### chord sent: Prefix )
/dev/ttys018 beta
### chord sent: Prefix (
/dev/ttys018 alpha

The client's pointer moved; nothing was detached and no process went anywhere. The command form is switch-client -t name, and it has a sharp edge worth meeting on purpose: it acts on "the current client," which exists only when one is attached. From your plain shell:

$ tmux -L t6 switch-client -t alpha
no current client
exit=1

Run it from inside a pane — where the TMUX environment identifies your client — or from outside with an explicit -c /dev/ttysXXX naming the client to switch; both forms verified in the same rig run. This command is the backbone of scripted session-jumping in §12.

Session chords

ChordCommand underneathWhat it does
Prefix ddetach-clientDetach the current client; the session keeps running
Prefix schoose-tree -ZsInteractive session picker for the attached client
Prefix $rename-session via promptRename the current session
Prefix (switch-client -pSwitch this client to the previous session
Prefix )switch-client -nSwitch this client to the next session
Prefix Dchoose-client -ZPick a client to detach — for when you are not the only one attached (§8)

"Command underneath" is the shipped default binding, read off this machine's tmux list-keys -T prefix. §11 teaches that table's key-table machinery; the cheat sheet (§17) repeats these cards. The Prefix playground after §7 simulates every row here except D (it has no second client to pick from) — drill them without a server.

How sessions end

  • kill-session -t name — the deliberate ending: every process in every pane is killed, visibly, on purpose. The counterpart of the orphan you can never reach again from §2.
  • Killing the last session exits the serverexit-empty on, the rule §4 demonstrated from both directions.
  • Detaching never ends anything. Neither does closing the terminal, or the client dying mid-SSH. All three remove a client; the session is untouched.
  • If the session you are watching dies, your client is detacheddetach-on-destroy on, the default. Verified live: a client attached to alpha, kill-session -t alpha, and the client process exited cleanly while the server and the other session carried on (list-clients empty, exit 0). Set detach-on-destroy off and the client would instead be switched to the most recently used surviving session — useful for pickers that kill what they leave.

Checkpoint: the basics, verified

Four cards on what this section actually taught. Answer out loud, then reveal. These are for you now — the full-course quiz lives in §20.

1Your server has zero sessions because exit-empty is off. What does tmux ls print, and with what exit status?
Nothing at all, exit 0 — a live server with nothing to list. The no server running error (exit 1) belongs to a dead server. Both were captured live in §4.
2Two clients are attached to alpha. A third runs tmux attach -d -t alpha. What does list-clients show afterward?
One row — the newcomer. -d detached both earlier clients on arrival. The session itself never changed; their terminals simply fell back to their shells.
3Attached to alpha, you press Prefix ) then Prefix (. Where are you, and what happened to your terminal?
Back on alpha, in the same terminal — ) switched the client to beta, ( switched it back. Session switching moves a pointer; it detaches nothing and moves no process.
4tmux ls prints beta: 1 windows (created …) (attached). What is the final marker telling you?
At least one client is currently attached to beta — the mirror state. It says nothing about who, from where, or at what size; list-clients (§8) answers those.
Lab 3 · Three sessions, one terminal
7 minutes · local · throwaway

A project, a scratchpad, a monitor — the three-workspace habit, built and toured on the throwaway -L lab server. Every command keeps its -L; your default server never appears.

1. Create the three, detached:

tmux -L lab -f /dev/null new-session -d -s project
tmux -L lab new-session -d -s scratch
tmux -L lab new-session -d -s monitor

Expect: silence, three times — and tmux -L lab ls lists all three in creation order, in the shape of the fast-pass transcript above.

2. Drop into the middle of the stack:

tmux -L lab attach -t scratch

Expect: scratch fills the terminal. The status line's left end now reads [scratch] — the session name in brackets, per the stock status-left. It does not list the other sessions; the interactive list is what s will offer.

3. Tour without detaching — press Prefix ) three times, slowly, watching the status line:

Expect: the current session advances monitorproject → back to scratch. The ring wraps, your terminal never detaches, and the third press lands you exactly where the tour began.

4. Land somewhere by name — Prefix s, type proj, Enter:

Expect: the tree filters to project as you type; Enter switches to it. This is switch-client wearing a picker.

5. Rename where you stand — Prefix $, type lab-project, Enter, then from your plain shell after detaching:

$ tmux -L lab ls

Expect: lab-project has replaced project — with its original creation timestamp, like the fast-pass rename.

6. Kill a session you are not attached to, then one you are:

tmux -L lab kill-session -t scratch

Expect: nothing happens to your client — you are attached to lab-project, and only its death would detach you. Then, inside monitor after switching to it, run tmux -L lab kill-session -t monitor from a second terminal: that client detaches and its terminal returns to the plain shell, while lab-project carries on.

Checkpoint:

  • The ) tour wrapped the ring and landed back on scratch — three switches, zero detaches
  • The rename kept the creation timestamp
  • You can say which session's death would detach your client, and why (detach-on-destroy on)

Teardown: tmux -L lab kill-server, then tmux -L lab ls — the no-server receipt, exit 1.

Sources: tmux(1) — new-session, attach-session (-d), switch-client, rename-session, kill-session, detach-on-destroy; DEFAULT KEY BINDINGS · bindings read from tmux list-keys -T prefix on 3.7b · transcripts: research captures 2026-08-22 (fast pass) and fresh -L t6 captures 2026-08-23 (mirror, steal, chords, kill-own-session), throwaway servers only

6 · Windows

A window is the tab-like unit from the map (§1): one full-screen view, one visible at a time, holding panes. If that sentence still feels like a definition to memorize rather than a thing you have, this section fixes it — because the window is also where tmux's option scoping, its status-line language, and its most counterintuitive command (link-window) all show up for the first time.

Create, name, navigate

Inside an attached session, Prefix c creates a window and makes it current. From outside, name it as you create it:

$ tmux -L lab -f /dev/null new-session -d -s alpha
$ tmux -L lab new-session -d -s beta
$ tmux -L lab new-window -t alpha -n logs
$ tmux -L lab new-window -t alpha -n edit
$ tmux -L lab list-windows -t alpha -F '#{window_index}: #{window_name} #{window_flags}'
0: zsh
1: logs -
2: edit *

Read that listing carefully; it teaches three things. The first window was never named, so tmux named it for you: zsh, the running program — automatic renaming, on by default, tracking whatever the pane is running. Your named windows keep their names. The * marks the session's current window and - the last-used one; selecting alpha:1 swaps them, live:

$ tmux -L lab select-window -t alpha:1
$ tmux -L lab list-windows -t alpha -F '#{window_index}: #{window_name} #{window_flags}'
0: zsh
1: logs *
2: edit -

Exact targets matter when no client is attached: an untargeted command acts on the server's current session — the most recently used one — which may not be the one you mean. Every transcript here carries an explicit -t; make it a reflex.

Window chords

ChordCommand underneathWhat it does
Prefix cnew-windowNew window, made current
Prefix ,rename-window via promptRename the current window
Prefix n / pnext-window / previous-windowStep through the window ring
Prefix 09select-window -t :=NJump straight to an index
Prefix llast-windowBack to the - window — the two-window ping-pong
Prefix wchoose-tree -ZwInteractive window (and session) tree
Prefix 'prompt, then select-window -t :NJump to a typed index
Prefix .prompt, then move-windowMove the current window to a typed index
Prefix &confirm-before kill-windowKill the current window (with a y/n prompt)
Prefix M-n / M-pnext/previous by alertJump to the next window with a bell or activity marker

Bindings read off this machine's tmux list-keys -T prefix; the man page's DEFAULT KEY BINDINGS section documents the same table.

Reorder: move and swap

Indexes are positions, and positions are yours to edit. The command form is move-window -s from -t to, and its edges are worth meeting once so they never surprise you:

$ tmux -L t6 move-window -s alpha:2 -t alpha:1
index in use: 1
$ tmux -L t6 move-window -s alpha:2 -t alpha:5
$ tmux -L t6 list-windows -t alpha -F '#{window_index}: #{window_name} #{window_flags}'
0: zsh
1: logs -
5: edit *

A move into an occupied index is refused — same error you will meet again in §7 — but a free index is taken literally: edit now lives at 5, and nothing renumbers to fill the gap. That is renumber-windows off, the default. Two cleanup shapes:

$ tmux -L t6 swap-window -s alpha:1 -t alpha:5
$ tmux -L t6 list-windows -t alpha -F '#{window_index}: #{window_name} #{window_flags}'
0: zsh
1: edit -
5: logs *
$ tmux -L t6 move-window -r -t alpha
$ tmux -L t6 list-windows -t alpha -F '#{window_index}: #{window_name} #{window_flags}'
0: zsh
1: edit -
2: logs *

swap-window exchanges two occupied slots — the honest way to reorder. move-window -r compacts the session's windows into sequence from base-index (0 by default) — the post-cleanup sweep. Rename is the last verb: rename-window -t alpha:2 drafts relabels in place, or Prefix , from inside.

The status-line flag decoder

The window list in your status line is annotated with flag characters — and tmux never explains them anywhere you would notice. The man page documents the #{window_flags} format variable but not the letters, so every row below was generated live, on purpose, on a throwaway server:

FlagMeansHow it was generated
*current windowany listing — see the transcripts above
-last-used window (before the current one)same — swaps with * when you select another window
!a bell rang in this windowprintf '\a' sent into a background window; flag appeared within 2 s
#activity since you last lookedmonitor-activity on for that window, then output in it
~silence — no output for the monitor-silence intervalmonitor-silence 3, then 4 s of nothing
Zthis window's pane is zoomed (§7)zoom a pane, then list another session's view of it
Mcontains the marked pane (§7)select-pane -m; cleared with -M
$ tmux -L t6 list-windows -t alpha -F '#{window_index}: #{window_name} flags=#{window_flags} marked=#{window_marked_flag}'
0: zsh flags= marked=0
1: logs flags=* marked=0
2: edit flags=!-M marked=1

One window wearing three flags at once: bell (!), last-used (-), marked pane (M). Two footnotes from the same run. Flags stack in one string per window. And in format output — as opposed to the status line — # is escaped to ##, so an activity-and-silence-flagged current window printed flags=##~*: activity, silence, current. The status line shows the single character; the escaping only bites when you parse format strings (§12).

Scoping, first contact

That flag table quietly introduced the guide's first non-global options: monitor-activity and monitor-silence were set with set-window-option -t alpha:idle … — per window. tmux options come in scopes (server, session, window, pane), and the defaults live in a global layer that any window without its own value falls back to. You set base-index and renumber-windows per session; you set monitor-activity per window. The full machinery — -g, the live global fallback, why order matters — is §10's subject. Until then, notice the pattern: some options follow the object they name.

link-window: one window, two sessions

A window normally belongs to exactly one session. A link puts one window object into a second session's list at an index you choose — both sessions now view the same panes, the same processes. This is how you keep a monitor window available everywhere without duplicating anything. And it is the most instructive failure in this half of the guide. First, the form everyone tries — link alpha's logs window into beta under a new name:

$ tmux -L lab link-window -s alpha:1 -t beta:linked
can't find window: linked
The target of link-window is a slot, not a name to create. -t addresses an existing window position by index — session:index. It will not create a window named linked; it looks for one, does not find it, and says exactly that. The working form names the index you want the link to occupy:
$ tmux -L lab link-window -s alpha:1 -t beta:1
$ tmux -L lab list-windows -t beta -F '#{window_index}: #{window_name} #{window_flags}'
0: zsh -
1: logs *

logs — the very same window, same panes, same shells — is now beta:1 and current there. The index is per-session: linking alpha:2 into beta at index 9 put the same window at beta:9 in the research run, while it stayed alpha:2. One object, two addresses. Unlink with unlink-window -t beta:9; the link dissolves and alpha still has its window. A window may not be linked to zero sessions — the last link out is the window's death (or use -k to make that explicit).

When a pane dies: remain-on-exit, introduced

One window-scoped option earns an early introduction because it rewrites the default death rules. Normally, a pane whose process exits is destroyed, and the last pane out takes the window and possibly the session with it. With remain-on-exit on — a window option — the pane stays, dead, showing its last output and a line tmux writes itself:

$ tmux -L dp display-message -p -t dp:0.0 'dead=#{pane_dead} dead_status=#{pane_dead_status} cmd=#{pane_current_command} flags=#{pane_flags}'
dead=1 dead_status=0 cmd=zsh flags=*
$ tmux -L dp capture-pane -p -t dp:0.0 | grep -n . | tail -2
1:macbook% exit
24:Pane is dead (status 0, Sat Aug 22 23:54:59 2026)

From the troubleshooting research run: #{pane_dead} is the probe, and the pane's last line literally reads Pane is dead (status N, …). A dead pane is recoverable — respawn-pane -k restarts it — and a window whose pane died unannounced shows as zsh[dead] in listings. Full treatment, including why a pane in the same window inherits the option: §16.

Lab 4 · Four windows and a shared one
8 minutes · local · throwaway

Build a real window set, draw what you think it looks like, then make the terminal agree — and finish by sharing one window into a second session.

1. Create the session and its named windows:

tmux -L lab -f /dev/null new-session -d -s proj
tmux -L lab new-window -t proj -n edit
tmux -L lab new-window -t proj -n logs
tmux -L lab new-window -t proj -n tests

Expect: four windows after the dust settles — index 0 still automatically named zsh, then your three named ones.

2. Before checking, draw the status line on paper: indexes, names, and which flag each window should carry, given that tests was created last and nothing else has happened.

3. Check:

$ tmux -L lab list-windows -t proj -F '#{window_index}: #{window_name} #{window_flags}'
0: zsh
1: edit
2: logs -
3: tests *

Expect: exactly your drawing — * on tests (created last, so current), - on logs (the current window tests displaced), and nothing at all on zsh or edit. One * and one - per session, always. If your drawing differed, re-read the flag decoder before continuing; the rest of the guide assumes it.

Block re-captured 2026-08-23 on a fresh throwaway server running this exact sequence; shown under the lab's socket name.

4. Reorder and compact: move tests to index 8, swap it with logs, then renumber:

tmux -L lab move-window -s proj:3 -t proj:8
tmux -L lab swap-window -s proj:2 -t proj:8
tmux -L lab move-window -r -t proj

Expect: the same four windows at 0–3 with logs and tests exchanged — 2: tests -, 3: logs * — and the - having followed the displacement, not the name. The occupied-index move error from this section is yours to reproduce by aiming move-window at an occupied slot.

5. Share one window. Create a second session and try the intuitive link first:

tmux -L lab new-session -d -s shared
tmux -L lab link-window -s proj:2 -t shared:logs

Expect: can't find window: logs — the same refusal as beta:linked above. Then the working form:

tmux -L lab link-window -s proj:2 -t shared:1
tmux -L lab list-windows -a

Expect: the all-sessions listing shows every window on the server, logs appearing in both proj and shared — in the shape of the research run's alpha:1: logs panes=3 … rows. Compare it to your drawing from step 2, updated.

Checkpoint:

  • Your step-2 drawing matched the step-3 listing, flags included
  • The named link failed and the indexed link worked — and you can say why before revealing the callout above
  • list-windows -a matches your updated drawing: one window, two addresses

Teardown: tmux -L lab kill-server. The shared session dies with its own last window; the unlinked proj windows are all in one server, so one kill-server is the whole cleanup.

Sources: tmux(1) — WINDOWS AND PANES; new-window, move-window, swap-window, link-window, unlink-window, remain-on-exit; window_flags format · transcripts: research captures 2026-08-22 (naming, flags, linking) and fresh -L t6 captures 2026-08-23 (move/swap/renumber, flag generation), throwaway servers only

7 · Panes

Everything above the pane is organization. The pane is a pseudo-terminal with one process inside it — the only place in the entire object tree where your work actually runs. Which means two things drive this section: how panes multiply and move (splits, layouts, resize, zoom, break and join), and the one distinction that makes all of it tractable — a pane's identity versus its index. Get that distinction and even the weirdest pane command reads cleanly.

Pane layout and identity: one window, three panes, indexes versus ids, and layout cycling window — three panes one pane = one pty = one process pane %0 · index 0 40x24 the original pane; every split carved out of a neighbor pane %1 · index 1 39x12 born of Prefix % pane %2 · index 2 39x11 born of Prefix " from %1 80 cols = 40 + 1 divider + 39 · 24 rows = 12 + 1 divider + 11 Prefix Space same three panes, next layout index 0 · %0 80x8 index 1 · %1 80x7 index 2 · %2 80x7 even-vertical: full width, near-thirds index — a position per window · starts at 0 · renumbered when panes close: kill index 1 above and %2 relabels from 2 to 1 id %N — an identity server-global · never reused · survives moves and joins · visible inside the pane as $TMUX_PANE
The pane map. Left: one window and its three panes with both labels — the durable id (%0, %1, %2, from a fresh-server capture) and the positional index. Right: what Prefix Space does — the same three panes, re-dealt; sizes shown are the research run's. Below: the distinction the rest of this section leans on. Later references point back here as the pane map (§7).

Splits, and where the columns go

Prefix % splits the current pane left-and-right; Prefix " splits it top-and-bottom. The command forms are split-window -h and split-window -v, and the flags name the arrangement: horizontal = panes side by side, vertical = panes stacked. (The divider between them runs the other way — a -h pair is separated by a vertical line — which is how everyone misremembers it once.) Watch the arithmetic on a real window, 80x24 with no client attached:

$ tmux -L lab split-window -h -t alpha:1
$ tmux -L lab split-window -v -t alpha:1
$ tmux -L lab list-panes -t alpha:1 -F '#{pane_index} #{pane_width}x#{pane_height}'
0 40x24
1 39x12
2 39x11

Provenance: the 2026-08-22 research run captured these sizes with a wider format string; this exact pairing was re-captured 2026-08-23 on a fresh throwaway and matched number-for-number — as did the layout and resize listings below.

80 columns became 40 + 1 + 39: every divider eats one column. The right half's 24 rows became 12 + 1 + 11 the same way. Splits always halve the pane being split — the second split targeted the right pane, so the left pane never changed. Your window's numbers come from its size; the plumbing does not. The 80x24 itself is the default-size option talking, which matters more than it sounds — §8 shows who really decides a window's size.

Identity versus index

Every pane has two labels, and conflating them is the root of most pane confusion. The index is a position: per window, starting at 0, in creation order — the number Prefix q flashes on each pane. The id (%N) is an identity: assigned server-wide in creation order, never reused, and it follows the pane through every move. Watch a pane keep its id while losing its index — three panes, then the middle one killed:

$ tmux -L t6 list-panes -t p:0 -F '#{pane_id} index=#{pane_index} #{pane_width}x#{pane_height}'
%0 index=0 40x24
%1 index=1 39x12
%2 index=2 39x11
$ tmux -L t6 kill-pane -t p:0.1
$ tmux -L t6 list-panes -t p:0 -F '#{pane_id} index=#{pane_index} #{pane_width}x#{pane_height}'
%0 index=0 40x24
%2 index=1 39x24

Fresh capture, 2026-08-23. Killing index 1 destroyed pane %1; pane %2 — a different pane, born second — slid down into index 1 and healed the split. Ids are also how a pane knows itself: inside any pane, $TMUX_PANE expands to its own id.

Rule of thumb: indexes are for talking about position, ids are for talking about the pane itself. Scripts and bindings should target %N (or #{pane_id}) precisely because indexes shift under them — a lesson §12 builds on. The pid makes the same point from underneath — one process traced across its whole journey:

$ tmux -L lab list-panes -a -F '#{session_name}:#{window_index}.#{pane_index} pid=#{pane_pid} tty=#{pane_tty} cmd=#{pane_current_command} active=#{pane_active}'
alpha:0.0 pid=71262 tty=/dev/ttys025 cmd=zsh active=1
alpha:1.0 pid=71264 tty=/dev/ttys026 cmd=zsh active=1
alpha:2.0 pid=71268 tty=/dev/ttys027 cmd=zsh active=0
alpha:2.1 pid=73799 tty=/dev/ttys029 cmd=zsh active=1
alpha:2.2 pid=73802 tty=/dev/ttys030 cmd=zsh active=0
alpha:3.0 pid=71273 tty=/dev/ttys028 cmd=zsh active=1

Pid 71264 appears there as alpha:1.0 — the only pane of a window called broken. By the end of this section it will have been alpha:3.1, one address changed twice while the process never noticed.

Layouts: positions, re-dealt

tmux ships five preset arrangements — even-horizontal, even-vertical, main-horizontal, main-vertical, tiled — plus, on this 3.7b, their mirrored variants. Prefix Space cycles them; select-layout names one outright. A layout is purely a dealing of positions: pane ids and indexes keep their numbers, only the geometry changes:

$ tmux -L lab select-layout -t alpha:1 even-vertical
$ tmux -L lab list-panes -t alpha:1 -F '#{pane_index} #{pane_width}x#{pane_height}'
0 80x8
1 80x7
2 80x7

Same three panes as the splits transcript, full width, near-thirds — the right side of the pane map (§7). Layouts are the fast answer to "just make this tidy"; manual resize is the slow answer with exact control.

Resize moves a boundary — nothing else

$ tmux -L lab resize-pane -t alpha:1.0 -R 10
$ tmux -L lab list-panes -t alpha:1 -F '#{pane_index} #{pane_width}x#{pane_height}'
0 80x8
1 80x7
2 80x7

That is the complete output of a resize that did nothing — by design, and it teaches the whole model:

Misconception: "resize makes a pane bigger." Resize moves an existing split boundary, in a direction, by cells. In the even-vertical stack above, pane 0 spans the full width — there is no vertical boundary to its right, so -R 10 (right by 10) has nothing to move, and nothing happens. No error, no change. Give the same command a boundary to chew on and it works immediately — same window, shrinking pane 2 from below its neighbor:
$ tmux -L lab resize-pane -t alpha:1.2 -D 5
$ tmux -L lab list-panes -t alpha:1 -F '#{pane_index} #{pane_width}x#{pane_height}'
0 80x8
1 80x12
2 80x2

Pane 1 gained exactly the five rows pane 2 lost: boundary moved, cells conserved. From a keyboard, Prefix Ctrl+ (and the other three arrows) resizes the current pane by one cell per press, repeatable within repeat-time.

Zoom: one pane, temporarily everything

Prefix z zooms the current pane to fill its window; the other panes keep running behind it, invisible. It is a toggle — z again restores the layout exactly. The research run zoomed pane 1 of the three-pane window:

$ tmux -L lab resize-pane -Z -t alpha:1.1
$ tmux -L lab list-panes -t alpha:1 -F '#{pane_index} #{pane_width}x#{pane_height} flags=#{pane_flags}'
0 80x8 flags=
1 80x24 flags=*Z
2 80x2 flags=-

Pane 1 reports the full window size (80x24) and carries flag Z — the zoomed pane is also marked current, *. The other panes still exist at their old sizes behind it. When zoom beats a bigger split: when you need to read, not watch — a diff, a log flood, a test run. When you need two things visible at once, zoom is the wrong tool and a split is the right one. Zoom is a lens, not a layout. One trap worth knowing now: a zoomed window advertises Z in the status line from any session's point of view (§6's decoder) — if you link that window elsewhere, the flag travels with it.

break-pane and join-pane: flags you will get backwards once

Promote a pane to its own window (break-pane); pull a pane from anywhere into the current window (join-pane). Both take -s and -t — and their meanings are inverted relative to intuition, differently from each other. This is the single most-misused command pair in tmux, so meet the failures first, on purpose:

$ tmux -L lab break-pane -t alpha:1.2
can't specify pane here
$ tmux -L lab break-pane -t alpha:1 -n broken
index in use: 1
break-pane's -t is the destination window, and -s is the pane. The intuitive reading — "break the pane I target with -t" — is exactly wrong. On 3.7b, from the local man page, verbatim: break-pane [-abdP] [-F format] [-n window-name] [-s src-pane] [-t dst-window] — "Break src-pane off from its containing window to make it the only pane in dst-window." So -t alpha:1.2 fails because a destination window must not carry a pane suffix, and -t alpha:1 fails because index 1 is occupied — add -a/-b to place after/before it instead. join-pane flips back to pane-level flags (-s src-pane -t dst-pane): the pane you join into is the one that gets split to make room.

The working break, placing the new window before index 1:

$ tmux -L lab break-pane -b -t alpha:1 -n broken
$ tmux -L lab list-windows -t alpha -F '#{window_index}: #{window_name} #{window_flags} panes=#{window_panes}'
0: zsh  panes=1
1: broken * panes=1
2: logs - panes=3
3: edit  panes=1

broken inserted at index 1 and everything else shifted up — logs from 1 to 2, edit from 2 to 3. Now, which pane did it take? The pid listing above was captured after the break, and it shows broken (alpha:1.0) holding pid 71264 — a server-start pid, sibling of alpha's originals (71262, 71268, 71273), each of which the same listing shows still in place in its own alpha window. By elimination, 71264 never belonged to an alpha window at all: it was beta's only pane. Two traps fired at once:

  • -s omitted means "the server's current pane." With no client attached, that belongs to the most recently used session — here beta, not alpha. The break carried away beta's only pane. Name your panes explicitly; every transcript-level surprise in this pair of commands comes from an implicit target.
  • A session cannot exist with zero windows. beta lost its last window and was destroyed by the break — the same last-one-out rule that ends servers (§4) and windows, one level up each time. After the break the server held exactly one session: alpha: 4 windows.

And the return trip — join-pane, pane-level flags, moving broken's pane into edit:

$ tmux -L lab join-pane -h -s alpha:broken.0 -t alpha:3.0
$ tmux -L lab list-windows -t alpha -F '#{window_index}: #{window_name} panes=#{window_panes} flags=#{window_flags}'
0: zsh panes=1 flags=
2: logs panes=3 flags=-
3: edit panes=2 flags=*
$ tmux -L lab list-panes -t alpha:edit -F '#{pane_index} #{pane_current_command} #{pane_width}x#{pane_height}'
0 zsh 40x24
1 zsh 39x24

The broken window is gone (its only pane left it — last-one-out again), edit now has two panes, and the traveler is pid 71264's shell: born beta's only pane, promoted to alpha:1.0, now alpha:3.1. Three addresses, one process, still running. Note the hole at index 1: renumber-windows off again — §6's move-window -r sweeps it. From a keyboard, Prefix ! breaks the current pane (no targets to misread); there is no default join chord — §10 binds one.

synchronize-panes: type everywhere at once

A window option that sends every keystroke to every pane in the window. On, off, and proof — one send-keys landing in both panes of edit, each pane identifying itself by its own id:

$ tmux -L lab set-window-option -t alpha:edit synchronize-panes on
$ tmux -L lab show-window-options -t alpha:edit synchronize-panes
synchronize-panes on
$ tmux -L lab send-keys -t alpha:edit 'echo "keys received in pane $TMUX_PANE"' Enter
$ tmux -L lab capture-pane -p -t alpha:edit.0 -S -10
docs/tmux-guide guides
❯ echo "keys received in pane $TMUX_PANE
"
keys received in pane %3

docs/tmux-guide guides
❯ echo "keys received in pane $TMUX_PANE
"
keys received in pane %3

docs/tmux-guide guides
❯
$ tmux -L lab capture-pane -p -t alpha:edit.1 -S -10
docs/tmux-guide guides
❯ echo "keys received in pane $TMUX_PAN
E"
keys received in pane %1

docs/tmux-guide guides
❯ echo "keys received in pane $TMUX_PAN
E"
keys received in pane %1

docs/tmux-guide guides
❯

Both panes ran the same line — %3 and %1, two ids, one keystroke. The doubled commands are real: the research run sent the demo twice. The broken-looking $TMUX_PAN / E" in pane 1 is also real — the command wrapping across its 39 columns, nothing more. And note the ids are not in window order: ids are global creation-order, indexes are local position.

It is called synchronize for a reason. With it on, everything you type goes everywhere — including rm, including Ctrl+c, including the exit that would close N panes at once. Turn it on for the burst of parallel work, and off the moment the burst ends: set-window-option synchronize-panes off. The status line shows the window as sync while it is on — check it before you type.

A word on the mouse

Stock tmux ships with mouse off — the options dump says so — and every pane action above has a keyboard form precisely because of that. Turn it on (set -g mouse on, full treatment in §10) and the terminal's mouse reaches into tmux: click selects a pane, drag on a divider resizes it, drag over text selects and copies, and the wheel scrolls — the default bindings route wheel-up into copy-mode, so scrollback just works. Two honest caveats: mouse text selection and terminal-native selection fight each other (hold Shift/Fn to let the terminal win, per your terminal's rules — §9 covers the clipboard fallout), and long sessions that depend on the mouse quietly lose the keyboard fluency this guide is building.

Pane chords

ChordCommand underneathWhat it does
Prefix %split-window -hSplit left-and-right
Prefix "split-window -vSplit top-and-bottom
Prefix oselect-pane -t :.+Next pane, in order
Prefix ;select-pane -lPreviously active pane — the two-pane ping-pong
Prefix qdisplay-panesFlash each pane's index on screen
Prefix Spacenext-layoutCycle the preset layouts
Prefix Ctrl+///resize-paneMove the split boundary, one cell per press
Prefix zresize-pane -ZToggle zoom on the current pane
Prefix { / }swap-pane -U / -DSwap the current pane with its neighbor
Prefix !break-panePromote the current pane to its own window
Prefix xconfirm-before kill-paneKill the current pane, with a y/n prompt
Prefix m / Mselect-pane -m / -MMark the current pane / clear the mark — the target for swap-pane and joins

Bindings verified against this machine's tmux list-keys -T prefix on 3.7b. Killing the last pane of a window takes the window with it — last-one-out, all the way up the tree. The Prefix playground after this section simulates the splits, focus, layout, zoom, kill, and break chords on a fake server; resize, swap, and mark answer "not modeled" there.

Lab 5 · Pane calisthenics
8 minutes · local · throwaway

From a bare window to the exact arrangement in the pane map (§7) — left pane full height, right column split in two — using chords only. Then bend it, zoom it, break it, and join it back.

1. Start clean and attach:

tmux -L lab -f /dev/null new -s gym

Expect: one session, one window, one pane, you attached. Draw the target on paper first: three panes, indexes 0/1/2, in the map's arrangement.

2. Build it with two chords: Prefix %, then Prefix ".

Expect: after %, two side-by-side panes with the new one current; after ", the current (right) pane splits top-and-bottom. The arrangement matches your drawing. Your exact dimensions depend on your terminal's size — the map's 40/39 and 12/11 came from an 80x24 window; the structure is the target. (§8 explains why your terminal gets a vote.)

3. Verify from the shell, after detaching with Prefix d:

$ tmux -L lab list-panes -t gym:0 -F '#{pane_id} index=#{pane_index} #{pane_width}x#{pane_height}'

Expect: three rows: pane 0 tall on the left, panes 1 and 2 stacked right, and ids in creation order — the same shape as the identity transcript in this section, modulo your window size. Re-attach with tmux -L lab attach.

4. Focus tour: Prefix o three times; then Prefix ; twice.

Expect: o walks the ring 0 → 1 → 2 → 0; ; bounces you between the two most recently focused panes.

5. Move a boundary: Prefix Ctrl+ a few times on a right-column pane; then give up on tidiness and press Prefix Space repeatedly.

Expect: each arrow press moves one boundary one cell; Space cycles the preset layouts — watch the indexes stay put while positions re-dealt, exactly as the map promises.

6. Zoom: Prefix z, look around, Prefix z again.

Expect: the current pane fills the window and the status line's window name gains a Z; the toggle restores your arrangement untouched. From outside, tmux -L lab display-message -p -t gym:0 '#{window_zoomed_flag}' reads 1 zoomed, 0 restored.

7. Break and rejoin — the commands, used precisely:

tmux -L lab break-pane -s gym:0.1 -t gym:1
tmux -L lab list-windows -t gym -F '#{window_index}: #{window_name} panes=#{window_panes} #{window_flags}'

Expect: the pane you named with -s — explicit, so no "current pane" surprises — becomes the only pane of a new window at index 1, and window 0 drops to two panes. Then join it back and watch the window count return to one:

tmux -L lab join-pane -s gym:1.0 -t gym:0.0

Checkpoint:

  • Two chords produced the map's arrangement, and list-panes agreed with your drawing
  • You can point at each pane and say its id and its index — and which of the two just changed when you pressed Space
  • The break used -s for the pane and -t for the destination window, and you can recite why the intuitive reading fails

Teardown: tmux -L lab kill-server — one session, one server, one receipt.

Sources: tmux(1) — split-window, resize-pane (-Z), select-layout, break-pane, join-pane, synchronize-panes, pane_index/pane_id formats; DEFAULT KEY BINDINGS · bindings read from tmux list-keys on 3.7b · transcripts: research captures 2026-08-22 (splits, layouts, resize, zoom, break/join, synchronize) and fresh -L t6 captures 2026-08-23 (id-vs-index kill), throwaway servers only

Prefix playground

Reading about chords builds vocabulary; fingers build reflexes. This unnumbered interlude is a small tmux living in the page — a state machine mirroring the object tree from the map (§1): server → sessions → windows → panes. Click it, press Prefix chords, and watch the same objects move: splits deal pane boxes, d detaches, s and w open the choose lists, and the status bar answers with the flag language §6 decoded.

How to drive it

  • Click the terminal below — it takes keyboard focus (Tab reaches it too). The hint strip inside confirms what the prefix currently is.
  • Press the prefix — stock is Ctrl+b — release it, watch [prefix] latch in the status bar, then press the key. Exactly the two-stroke rhythm §5 taught.
  • The toggle above the terminal swaps between two verified keymaps: stock C-b, and §10's opinionated build — C-a prefix, mnemonic splits, reload chord — plus the pane-nav block the playground ships alongside it (hjkl under the prefix, plain arrows without it). Reset reseeds the two-session state.
  • Escape releases focus from anywhere in the simulator — no keyboard trap. ? opens a keymap overlay listing whatever the current config binds.
Click the terminal, then press Prefix chords — a keyboard is required
[alpha] beta 0:zsh* 1:logs- [prefix]--:--
0:zsh%0
 
Click here, then press Prefix chords · stock prefix: Ctrl+b · Escape releases focus

Everything the simulator binds was read off a real server first: one throwaway -L sim -f /dev/null, list-keys -T prefix for the stock table, then §10's assembled file sourced in and list-keys again — the same method this guide uses everywhere. Two details the transcript settled: & is kill-window with a y/n prompt (, is the rename chord), and under the opinionated map l becomes select-pane -R — so last-window loses its chord there, exactly the displacement §10 teaches for - and r. Press ? inside the simulator to see the current map, displacements included.

What the simulator simplifies — on purpose. Panes are re-dealt by layout class (the five presets from §7); real splits carve the current pane in half, and §7's arithmetic is the honest account. The [prefix] latch fades after 750 ms so a stray latch never leaves you guessing — real tmux never times the prefix out; it stays armed until your next keypress (750 ms is display-time, the clock status-line messages use). Pane contents are painted, not run — no shell lives in a box. And any guide-taught chord the simulator does not model answers "not modeled here" in the message line rather than silently doing nothing: honest over fake.

Sources: tmux(1) — DEFAULT KEY BINDINGS; choose-tree, confirm-before, command-prompt, display-panes, send-prefix · every simulator binding read from tmux list-keys -T prefix and -T root on 3.7b, stock and §10-plus-pane-nav keymaps — the 29 chords, M-1M-5 layouts, the send-prefix double-tap, and the root-table arrows each captured — throwaway -L sim and -L t11fix servers only, 2026-08-23/24 · timings (display-time 750, repeat-time 500, display-panes-time 1000) from the same servers' global session options

8 · Clients

Everything in §4 through §7 lived inside the server. The client is the part outside: a terminal plus one socket connection, and nothing else. This is the section where tmux's most confident folklore dies — because clients get a vote on window size, and the way they vote on current tmux is not the way the old stories tell it.

What a client is, precisely

A client is a process — the tmux attach you ran — connected to the server through the socket, drawing into exactly one terminal. Not the terminal itself; the terminal is the client's furniture. Ask the server who is connected, in tmux's own words:

$ tmux -L lab list-clients
/dev/ttys031: alpha [80x24 xterm-256color] (attached,focused,UTF-8)

One row per client: the tty it owns, the session it views, its terminal's size and $TERM (the contract from §2), and its state — this one is attached and holds focus. With no clients connected, the same command prints nothing and exits 0 — and with no server, it prints nothing and exits 1. Same silence, different meanings; the exit status is the tell, which is why scripts should check it rather than parse output:

Statelist-clients saysExit status
Server alive, clients attachedone row per client0
Server alive, no clientsnothing0
No servernothing1

All three observed — the empty-but-alive row in this guide's client research, the other two re-confirmed in fresh captures. Compare §4's socket-state table: same discipline, different question.

Who may attach, and what happens

Any number of clients may attach to the same session; they mirror it — same window, same pane, same cursor. Two sessions, one client? The client views one at a time and switches without detaching (§5). The patterns worth having names for:

PatternYou runResult
One session, two clientstmux attach -t shared in a second terminalMirror — both see everything; whoever types, types for both
Two sessions, one clientPrefix s / ( / )The client's pointer moves; no second connection
Take the session backtmux attach -d -t sharedSteal — every other client is detached as you arrive
Watch without typingtmux attach -r -t sharedRead-only client — input ignored (an attach-session flag, man-sourced)

The mirror row deserves its warning: a shared session is a shared keyboard. If two people are attached and one starts typing in the editor both are viewing, those keystrokes land once, for both. Pair-programming gold; accident risk otherwise. list-clients before you type.

Size negotiation: latest, not smallest

Here is the folklore: "when two clients share a session, the smallest terminal wins — everyone gets the little window." That was true of older tmux. On the 3.7 series it is opt-in, and the default is the opposite. Straight off this machine:

$ tmux -L lab show-options -g window-size
window-size latest
Misconception: "smallest terminal wins." window-size is a session option, and its default on 3.7b is latest: the window matches the most recent client to attach or resize, whoever that is and however small. A 100-column window happily persists while a 60-column client is attached — the small client just gets a viewport with scroll-off. The classic smallest-wins behavior still exists, but you ask for it: set-option -t SESSION window-size smallest. If you learned tmux from an older version or an older tutorial, this one option is why shared sessions no longer shrink.

The whole matrix, captured live in the research rig — client A attached on a 100x30 pty, then client B on 60x20, both to alpha; the window's dimensions probed after each step:

### A alone (100x30): dims
w=100 h=29
### B attached (60x20): dims (window-size=latest, B most recent)
w=60 h=19
### after A resized to 40x15 (A most recent)
w=40 h=14
### after A back to 100x30 (B is 60x20, still smaller)
w=100 h=29
### set window-size smallest  ->  w=60 h=19
### set window-size back to latest  ->  w=100 h=29

Each ### line is a label printed by the rig between real display-message outputs; the w=… h=… values are verbatim tmux output.

And the cast, listed partway through that same run — after step two, while both clients were attached:

$ tmux -L lab list-clients -F '#{client_name} #{client_tty} #{client_termname} #{client_session} #{client_width}x#{client_height}'
/dev/ttys031 /dev/ttys031 xterm-256color alpha 100x30
/dev/ttys032 /dev/ttys032 xterm-256color alpha 60x20

Read the fourth line twice: B — smaller — is attached, and the window is 100 wide anyway, because A touched its size last. That is latest working exactly as shipped. Two smaller observations in the same data: the off-by-one (a 100x30 client yields a 100x29 window — the status line eats one row, always), and the fact that client_width/client_height report each client's own terminal, while window_width/window_height report the negotiated window — the predict-then-check pair Lab 6 drills.

Three ways to detach (one of them accidental)

### client attached:
/dev/ttys031: alpha [80x24 xterm-256color] (attached,focused,UTF-8)
### closing the pty master (simulates closing the terminal window)
client exited rc=1
### session after terminal closed:
alpha: 3 windows (created Sat Aug 22 23:09:49 2026)
### list-clients:
''

The ### lines are the capture rig's step labels between real outputs; '' is the rig's rendering of empty output — no rows, exit 0.

That is the accidental one: close the terminal, and the client dies with it — while the session does not so much as blink. The deliberate ones: Prefix d detaches the client you are in, and detach-client does it surgically from outside:

CommandDetaches
detach-clientThe current client (from a binding) — what Prefix d runs
detach-client -s sharedEvery client attached to session shared
detach-client -aAll clients on the server
detach-client -a -t /dev/ttys031All except the named client

The last three follow the man page's synopsis, verified against 3.7b docs; the first and the -a form are transcript-proven ("detach-client -a — detaches everyone; list-clients empty immediately after; session survives").

switch-client versus detach/attach

Both change what your terminal shows. They differ in what happens to the connection:

switch-clientdetach, then attach
Connection to serverStays upDropped, then rebuilt
Your shell shows throughNever — the switch is instantYes, between the two commands
Works without a clientNo — no current client (§5)Yes — attach creates one
Typical useHopping between your own sessionsComing back later, or from another machine

One more client state rounds out the set: suspended. Prefix Ctrl+z runs suspend-client — the client process is stopped and your terminal falls back to the shell that launched tmux, with the session still attached server-side. Type fg in that shell to resume. It is the "let me run one command outside without losing my place" move, and unlike detach it keeps the connection. (Binding verified on 3.7b; the man page's binding table documents it as "Suspend the tmux client.")

Client chords

ChordCommand underneathWhat it does
Prefix ddetach-clientDetach this client
Prefix Dchoose-client -ZPick which client to detach, from a list
Prefix Ctrl+zsuspend-clientSuspend this client; fg in the shell resumes it
Prefix rrefresh-clientForce a redraw — for when the terminal garbles
Lab 6 · Two terminals, one session
6 minutes · local · two terminal windows · throwaway

The client research in this section was driven by scripted pseudo-terminals; this lab is the hands-on version — two real terminal windows, your mouse doing the resizing, and your predictions on the line before every check.

1. Open two terminal windows. In the first:

tmux -L lab -f /dev/null new -s shared

In the second:

tmux -L lab attach -t shared

Expect: both windows show the same session — type in either, watch both. Two rows in tmux -L lab list-clients, both naming shared.

2. Shrink the second window's terminal — drag it to roughly half width. Before checking, write down your prediction for the session's window size. Then:

tmux -L lab display-message -p -t shared '#{window_width}x#{window_height}'
tmux -L lab list-clients -F '#{client_width}x#{client_height}'

Expect: the window matches the terminal you just resized — latest at work, the same rule that kept a 100-wide window while a 60-wide client watched in this section's matrix. Each client row reports its own terminal; one row disagrees with the window, and it is not the one you touched. Your height is one less than your terminal's rows: the status line.

3. Resize the first terminal instead — even slightly. Predict, then check again.

Expect: the window snaps back toward the first terminal's size. The vote always goes to the most recent toucher, big or small.

4. Opt into the old rule:

tmux -L lab set-option -t shared window-size smallest

Expect: the window immediately shrinks to the smaller of the two terminals — the behavior the folklore promised, now visibly a choice. Restore the default with window-size latest and watch it snap back.

5. Steal the session from a third terminal (or either one, after detaching it with Prefix d):

tmux -L lab attach -d -t shared

Expect: every other client detaches at once; list-clients shows one row. Their terminals are back at their shells; the session never noticed.

Checkpoint:

  • Both predictions in steps 2–3 were right — and you can state the default (window-size latest) and what "most recent to attach or resize" means
  • You watched smallest shrink the window and latest restore it — opt-in, not default
  • You can explain the off-by-one between client height and window height without checking

Teardown: tmux -L lab kill-server from any terminal.

Sources: tmux(1) — attach-session (incl. -d, -r), detach-client, switch-client, suspend-client, window-size; client_width/client_height/window_width/window_height formats · transcripts: research captures 2026-08-22 (sizing matrix, default format, detach methods, zero-clients) and fresh -L t6 captures 2026-08-23 (mirror, steal, no-server exit status), throwaway servers only

9 · Copy-mode & buffers

A pane is a terminal emulation, not a text file. The program inside it owns the keyboard and the visible grid; everything that scrolled off the top still exists, but only in a history buffer the server keeps on the pane's behalf. Reading that history needs keys — and every key already belongs to your program. tmux resolves the standoff by borrowing the keyboard back: a mode, entered on purpose, in which the same physical keys suddenly mean move, search, select, copy. This section is that mode, the buffer stack it feeds, and the two further leaps text must make to reach your system clipboard.

Where text lives: pane scrollback, the server-wide buffer stack, and the system clipboard outside tmux tmux server one process — everything in here is RAM (§4) pane %N — scrollback the pane's own history, kept by the server history-limit 2000 lines (stock) dies with the pane read it: copy-mode buffer stack — per server paste buffers, newest first: buffer-limit 50 (stock) shared by every session feed it: any copy command copy Enter paste prefix ] system clipboard outside tmux entirely: your terminal, your OS macOS: pbcopy/pbpaste over ssh: OSC 52 bytes reached only via a client export OSC 52 · pipe The export arrow is the only one that leaves the server: the bytes ride a client's terminal (§2's contract), which is why the same copy behaves differently local vs ssh. scrollback is per pane · buffers are per server · clipboard is not tmux's at all capture-pane -b: pane → buffer, straight · capture-pane -p: pane → stdout, clean
The copy map. Three places text can live, two of them inside the server: a pane's scrollback (private to that pane, capped by history-limit) and the buffer stack (server-wide, shared by every session, capped by buffer-limit). The system clipboard sits outside tmux altogether, and the only road to it runs through a client's terminal. Later sections point back here as the copy map (§9).

Enter, exit, and the key that does neither

Prefix [ enters copy-mode on the current pane; the binding underneath is plain copy-mode. The pane's border changes color while you are in it — the stock pane-active-border-style is a live conditional that turns yellow in a mode — so the state is visible from anywhere in the window. Two other roads in: with mouse on (§10), rolling the wheel up enters copy-mode scrolled back one page (the stock root-table binding routes MouseDown1ScrollbarUp to copy-mode -u), and scrolling with the wheel in a stock terminal does nothing inside tmux, because stock tmux ships mouse off — the wheel belongs to your terminal's own scrollback, which inside tmux is usually the wrong buffer. (The Prefix playground after §7 rehearses the prefix latch this chord rides on; copy-mode itself is not modeled there.)

Leaving is where the surprises live. The exits, verified key by key on 3.7b:

Keyvi tableemacs tableVerified how
qcancel — leave, nothing copiedcancel — samelive: #{pane_in_mode} went 1 → 0
Entercopy-pipe-and-cancel — copy, then leavenot bound — M-w or C-w is the copy keyvi live: a buffer appeared and the mode ended; emacs per list-keys
Escapeclear-selection only — you stay in the modecancel — leaves (per the binding table)vi live: still in mode; emacs per list-keys
Misconception: "Escape gets me out of copy-mode." It does in emacs mode, and it does in vim, and so everyone assumes. In vi copy-mode on 3.7b, Escape is bound to clear-selection and nothing more — a fresh capture proved it: enter copy-mode, press Escape, #{pane_in_mode} still reads 1; press q, it reads 0. If you take one habit from this section, make it q to leave. (Stuck-in-copy-mode is a troubleshooting symptom in §16 for exactly this reason.)
Copy-mode state machine: entry, exits, and the Escape self-loop root — normal typing every key goes to the program in the pane border: green · status: normal copy-mode(-vi) the pane's keyboard, retasked: move · search · select · copy border: yellow — pane-active-border-style is a live conditional (§14) cursor starts at the bottom of view Prefix [ wheel up (mouse on) → copy-mode -u q — leave, nothing copied Escape (vi): clears selection — stays in Enter (vi) · M-w (emacs) buffer stack — newest pushed copy AND leave — one key, both effects prefix ] pastes it back (§9, buffers)
The copy-mode state machine. Two states, and the transitions between them — note that Enter does double duty (copy and leave, vi table) while q only leaves, and that Escape in vi mode is a self-loop, not an exit. Later references point back here as the copy-mode machine (§9).

vi or emacs: mode-keys follows your editor

Which table you get is one option: mode-keys, vi or emacs. Its default is not a constant — it is inherited from the environment of whoever started the server. The man page, verbatim: "Use vi or emacs-style key bindings in copy mode. The default is emacs, unless VISUAL or EDITOR contains 'vi'." Three fresh servers, three answers, one machine:

$ EDITOR=vim   tmux -L a -f /dev/null new-session -d -s s   # then: show-options -gwv mode-keys
vi
$ EDITOR=emacs tmux -L b -f /dev/null new-session -d -s s   # then: show-options -gwv mode-keys
emacs
$ env -u EDITOR tmux -L c -f /dev/null new-session -d -s s   # then: show-options -gwv mode-keys
emacs

Captured 2026-08-23, three throwaway servers, killed after (socket names shortened for print; the # comments mark the second command whose output follows). This shell exports EDITOR=vim (VISUAL unset), so every server started from it — including every transcript earlier in this guide — defaults to vi. Your machine may differ; ask yours with tmux show-options -gwv mode-keys before blaming the keymap.

Pin it explicitly and the ambiguity dies: set -g mode-keys vi (on 3.7b this lands in the global window options even though the -g spelling looks session-scoped — show-options -gw mode-keys confirms it; set -wg mode-keys vi says the same thing precisely). The opinionated build in §10 pins it; the rest of this section shows both spellings' keys side by side.

The copy-mode keymap

Both tables, read off tmux list-keys -T copy-mode-vi and -T copy-mode on 3.7b — the commands in the middle column are literally what each key runs:

Do thisvi (mode-keys vi)emacs (mode-keys emacs)
Move the cursorh j k l, w next word , M-b/M-f by word
Page and jumpC-u/C-d half-pages, g/G top/bottom of historyM-v/C-v pages, M-</M-> top/bottom
Search? prompt, search up · / prompt, search down · n repeat · #/* word under cursorC-r incremental, search up · C-s incremental, search down · N reverse
Begin a selectionSpace (like vim's visual) · v toggles rectangleC-Space · R toggles rectangle
Line ends0 / $C-a / C-e — same as your shell
Copy and leaveEntercopy-pipe-and-cancelM-w or C-w — same command
Copy and keep goingcopy-pipe via a custom bindingsame — bind your own
Leave, copy nothingqq or C-c
Misconception: "y yanks — it's vi." It is not vim; it is a keymap styled after vim, and on 3.7b plain y is simply unbound in copy-mode-vi — a grep over the live table finds no binding (only C-y, which scrolls up). Enter is the copy key, and it copies and leaves. If vim hands want y, bind it yourself — one line, §11 teaches the machinery: bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel. Until you do, muscle memory will press y, nothing will happen, and you will press Enter anyway.

What copying actually does — proven rather than promised. Three planted lines, copied with the keyboard flow above (search up to the anchor, start of line, select, down two, end of line, Enter):

$ seq -f 'LAB7 line %g' 1 3          # in the pane
$ tmux -L lab copy-mode -t lab:0      # entered; keys driven as above
$ tmux -L lab list-buffers
buffer1: 35 bytes: "LAB7 line 1\nLAB7 line 2\nLAB7 line 3"

Real round trip, 2026-08-23: the same command sequence every vi binding runs (each is send-keys -X <command> underneath), driven detached so the bytes could be captured. Note the byte count — 35, no trailing newline: $ stops at the last line's end. An earlier attempt in the same run searched for just LAB7, landed on line 3 — the nearest match above the cursor — and copied "line 3 + prompt + blank" instead. Anchor your search on the first line's full text; Lab 7 makes you feel the difference.

The buffer stack: server-wide, newest on top

Every copy lands in a paste buffer, and the buffers form a stack owned by the server — not the session, not the pane. All sessions on one server share one stack, newest listed first, each named bufferN with the highest N newest, up to buffer-limit 50 before old ones fall off. The stack, worked end to end on a throwaway server:

$ tmux -L cp -f /dev/null new-session -d -s cp
$ tmux -L cp set-buffer 'first' && tmux -L cp set-buffer 'second' && tmux -L cp list-buffers
buffer1: 6 bytes: "second"
buffer0: 5 bytes: "first"
$ tmux -L cp show-buffer
second
$ tmux -L cp set-buffer -b named 'named-content'; tmux -L cp list-buffers
named: 13 bytes: "named-content"
buffer1: 6 bytes: "second"
buffer0: 5 bytes: "first"
$ tmux -L cp delete-buffer -b named; tmux -L cp list-buffers
buffer1: 6 bytes: "second"
buffer0: 5 bytes: "first"

From the 2026-08-22 research run, verbatim. set-buffer pushes; a bare delete-buffer drops the top; -b names a buffer so scripts can address it deterministically. show-buffer prints the top buffer — with no trailing newline, so in a real terminal it runs into your next prompt.

The chords that work the stack, all verified against list-keys -T prefix on 3.7b:

ChordCommand underneathWhat it does
Prefix ]paste-buffer -pPaste the top buffer into the pane (bracketed — see below)
Prefix #list-buffersSee the stack, newest first
Prefix =choose-buffer -ZInteractive picker: browse, paste, delete any buffer
Prefix -delete-bufferDrop the top buffer

Paste: text in, Enter not included

A paste inserts bytes at the pane's input — it does not press Enter. What happens next depends entirely on what those bytes contain, and both halves were captured live. A buffer with no trailing newline simply parks on the input line: the research run's paste of "second" left the pane reading ❯ second — visible, editable, not executed until Enter arrived by hand. A multi-line buffer is where it bites — a raw paste's embedded newlines act as Enter, one per line:

### raw paste-buffer of a 3-line buffer into /bin/zsh -f:
1:macbook% LAB7 line 3
2:zsh: command not found: LAB7
3:macbook%
5:macbook% macbook%
6:zsh: command not found: macbook%
8:macbook%
### same buffer, paste-buffer -p (bracketed paste):
1:macbook% LAB7 line 3
2:macbook%

Read the two halves carefully. The raw paste ran two garbage commands — the buffer was "LAB7 line 3\nmacbook%\n\n", and each embedded newline acted as Enter: one error per line, then a bare prompt for the blank line (line numbers skip the empty rows). The bracketed paste ran nothing: no error lines at all. The whole buffer parked in the shell's input area — line 2's macbook% is the pasted text continuing, not a fresh prompt — waiting for your Enter. That contrast is why Prefix ] ships as paste-buffer -p: the -p wraps the paste in bracketed-paste markers, and a shell with bracketed paste on (zsh's is, by default) treats the whole thing as literal text. The command name for scripts and bindings is the same: paste-buffer -p -t target.

One more bridge belongs here. capture-pane turns pane content into text on demand — capture-pane -p prints the pane to stdout, the clean way to read a pane from outside; capture-pane -b name pushes it onto the buffer stack instead. The research run flagged the seam between them: a buffer made by capture-pane -b keeps the pane's trailing blank lines (its example buffer was "85 bytes" of mostly newlines), because a pane is a fixed grid of rows, empty ones included. -p to stdout keeps them too — pipe through grep -n . or sed when you want only the written lines.

Reaching the system clipboard

The buffer stack is inside the server; your clipboard is not. Crossing that line takes one of two roads, and knowing which road is which is most of the practical knowledge about tmux clipboards.

Road one: pipe through a program. The copy-command option names a shell command that copy-pipe-family actions run, selection on stdin. Out of the box it is empty — show-options -sv copy-command on this machine prints a blank value — so on 3.7b, vi Enter (copy-pipe-and-cancel) "pipes" to nothing and just behaves as copy-and-cancel. Set it locally and the same keystroke lands in the clipboard:

set -s copy-command 'pbcopy'      # macOS; Linux: xclip -selection clipboard, wl-copy

That is a server option (-s), one line, and it makes every copy-mode copy also reach the OS clipboard — on the machine where tmux runs. Which is the catch: over SSH, pbcopy runs on the remote host, where your clipboard is not.

Road two: OSC 52, the escape sequence that travels. An OSC 52 sequence is an instruction to whatever terminal displays it — "put these bytes in your clipboard." tmux emits it when the set-clipboard option allows (default external; on makes tmux also accept such sequences from programs inside panes) and the client's terminal advertises the capability — any client matching the stock terminal-features entry xterm*:clipboard does, and this machine's attached clients resolved exactly bpaste,ccolour,clipboard,cstyle,focus,RGB,title. Because the sequence is just bytes in the output stream, it crosses SSH like any other output — the copy you make on the server sets the clipboard of the laptop in front of you.

Under the hood: OSC 52 is real, asynchronous, and skip-prone. Live-captured on 3.7b (research rig, 2026-08-22): the exact bytes are ESC ] 52 ; ; <base64> BEL — the clipboard-target field is empty, and the terminator is BEL, so patterns demanding a target or ST miss it. The write is queued on the client's output buffer, not sent synchronously: one rig run saw nothing on the pty 0.8 s after the copy command, the sequence arriving only on the next redraw. And the copy-mode copy path itself can be skipped when a pane redraw is pending (selection highlights set exactly that flag; tmux's own source clears it in one special case and retries nothing). Practical upshot: clipboard-on-copy is opportunistic — when you need it deterministic, use set-buffer -w -t <client-tty> 'text', which writes the clipboard directly and was the rig's reliable hammer.
Drift: the over-SSH leg is docs-verified, not live-tested here. This guide's rule is that everything taught gets run on this machine. ssh localhost was attempted for exactly that and refused (Permission denied (publickey) — sshd reachable, no key for a non-interactive agent), so it was not forced. The mechanics above — the byte shape, the async queue, the redraw skip, the client-feature gate — are live-captured locally; the claim that OSC 52 crosses SSH unchanged is from the man page's set-clipboard description and tmux's source (tag 3.7b): the sequence is ordinary output, and SSH forwards ordinary output. Treat the SSH leg as high-confidence documentation, and test your own terminal pair before relying on it — terminals vary in whether they honor OSC 52 at all.
Lab 7 · Copy without the mouse
10 minutes · local · throwaway

Plant three lines, take them with the keyboard alone, then take them again with the mouse, then push one copy out to the system clipboard — one lap around the copy map (§9). You need two terminals: one attached to the session, one for verification commands.

1. Plant the lines:

tmux -L lab -f /dev/null new -s copy
seq -f 'LAB7 line %g' 1 3

Expect: the session fills your terminal and the three lines print, followed by your prompt. The lines now exist in exactly one of the map's three places: this pane's scrollback.

2. Keyboard-only copy. Press Prefix [; the border turns yellow. Then: ?, type LAB7 line 1, Enter — the cursor jumps to the first planted line. Now Space (begin selection), j j (down two lines), $ (to end of line), Enter (copy and leave).

Expect: the border returns to normal — one key, both effects, per the copy-mode machine (§9). From the second terminal:

$ tmux -L lab list-buffers
buffer0: 35 bytes: "LAB7 line 1\nLAB7 line 2\nLAB7 line 3"

Expect: exactly that line — the same round trip captured live while writing this section, byte count included. If yours differs, the likely culprits are the anchor (step 4 covers that) or the copy key — in vi mode it is Enter, not y.

3. Paste it back. In the attached terminal, clear the prompt with Ctrl+c, then Prefix ].

Expect: the three lines appear on your input line as one block — and nothing executes. That is paste-buffer -p: bracketed paste, Enter not included. Press Ctrl+c to discard, and note you now know why the paste did not run anything.

4. The wrong-anchor lesson, on purpose. Enter copy-mode again and search with ? for just LAB7 — not the full first line.

Expect: the cursor lands on line 3, not line 1: search-up starts from the cursor (which sits at the bottom of the view on entry) and takes the nearest match above. Copy from there and you harvest line 3 plus your prompt plus a blank line — the author's own first attempt in this section's live run produced exactly buffer0: 26 bytes: "LAB7 line 3\nmacbook%\n\n". Search on the full first line; the nearest match is then the right one. Press q to leave — not Escape, which in vi mode only clears a selection.

5. Mouse mode, same lines. From the second terminal:

tmux -L lab set -g mouse on

Then in the attached terminal, drag across the three planted lines and release.

Expect: on release, a new buffer sits on top of the stack — tmux -L lab list-buffers shows it first. The mechanism is verifiable without a mouse in your hand: the stock bindings route a drag start to copy-mode -M (selection begins) and the release to copy-pipe-and-cancel — read them yourself with tmux -L lab list-keys -T copy-mode-vi | grep MouseDrag. Mouse copy is keyboard copy wearing a pointer. Bonus from the same table, also stock: a double-click copies just the word under the pointer (DoubleClick1Pane runs select-word then copy-pipe-and-cancel).

6. Push it out of the server. The deterministic local road (macOS; on Linux pipe to wl-copy or xclip -selection clipboard instead), from the second terminal:

tmux -L lab show-buffer | pbcopy
pbpaste | head -1

Expect: LAB7 line 1 — the top buffer, now in the OS clipboard, one pipe outside the server. This is road one from the section, minus even the config: no option set, no OSC 52, just a program reading the buffer. The full-fidelity road (copy-command, set-clipboard and the OSC 52 over-SSH leg) is documented above with exactly what was and was not live-tested; if you have a real SSH box and a terminal you trust, verifying one OSC 52 copy end to end is the natural extension of this lab.

Checkpoint:

  • The keyboard copy produced the 35-byte buffer, and you can name every key that made it — including which key both copies and leaves
  • The paste executed nothing, and you can say why (bracketed paste, no Enter in the buffer)
  • You fell for (or dodged) the wrong-anchor search, and you left copy-mode with q — after proving to yourself Escape was not going to do it
  • Text you copied exists in all three of the map's places now — scrollback, buffer stack, clipboard — and you can point at which arrow moved it where

Teardown: tmux -L lab kill-server — buffers and scrollback are server RAM, so one command erases both. Your OS clipboard keeps what pbcopy received; that is the point of it.

Sources: tmux(1) — copy-mode, mode-keys, copy-command, set-clipboard, set-buffer, paste-buffer, capture-pane, choose-buffer; BUFFERS; DEFAULT KEY BINDINGS · tmux source, tag 3.7b — cmd-set-buffer.c, window-copy.c, screen-write.c, tty.c, tty-features.c (OSC 52 emission gates), accessed 2026-08-22 · transcripts: research captures 2026-08-22 (buffer stack, clipboard matrix, OSC 52 bytes) and fresh -L t7 captures 2026-08-23 (mode-keys env matrix, key tables, keyboard copy round trip, raw-vs-bracketed paste, Escape vs q), throwaway servers only

10 · Configuration

A tmux configuration is not a settings file that gets parsed; it is a script of tmux commands the server runs at startup — and every line of it also works, unchanged, in a running server. That one fact organizes the whole section: learn the commands (set-option and its scopes, bind-key, source-file), and "configuring tmux" collapses into "telling a running server what to do, then writing down what worked." At the end, a ~25-line opinionated config — argued one line at a time, cons included — that you can adopt, reload, revert, and re-adopt without ever restarting anything that matters.

The file is a script

At startup the server reads the first of these that exists — from the local man page's FILES section, verbatim: ~/.tmux.conf, $XDG_CONFIG_HOME/tmux/tmux.conf, ~/.config/tmux/tmux.conf, then the system-wide /usr/local/etc/tmux.conf. Every line is a command you could type after tmuxset-option, bind-key, source-file, comments starting with #. The flag every transcript in this guide relies on, -f /dev/null, simply points the startup script at nothing: stock tmux, zero configuration. Your everyday server is running a script right now; §11's list-keys and this section's show-options show you what it did.

Because the file is a script, there is no config format to learn and no reload magic: re-running the script is literally source-file ~/.tmux.conf. Two properties of that command shape everything downstream, both verified live in this section's research:

  • It applies to a running server. A server started stock was handed the opinionated config below mid-flight; show-options -gv prefix flipped from C-b to C-a with sessions alive and nothing restarted — every existing session obeying a later global, which is your first hint of the rule this section builds below.
  • Re-running is not resetting. After that, source-file /dev/null — sourcing nothing — left the prefix at C-a. The old commands already ran; running zero new ones changes nothing. To undo a setting you must set it back explicitly, or start a fresh server.

set-option and its scopes

Options live at four levels, and the flag picks the level:

ScopeSet it withShow it withExamples you have met
Serverset -sshow -sescape-time, buffer-limit, copy-command, set-clipboard, exit-empty (§4)
Session (and global session)set / set -gshow / show -gprefix, mouse, base-index, history-limit, window-size (§8)
Window (and global window)set -w / set -wgshow -w / show -gwmode-keys, remain-on-exit, synchronize-panes, monitor-activity (§6)
Paneset -pshow -pany window option, for one pane — the man page's own example: set -pt:.0 window-style bg=blue

The -g layers are the fallbacks, consulted live — the man page's words: "Sessions which do not have a particular option configured inherit the value from the global session options," and global window options are "a set … from which any unset window or pane options are inherited." An object with no value of its own reads the global at the moment the option matters, whenever that is. One 3.7b nicety verified live: set -g mode-keys vi — session-spelled, window-scoped option — is accepted and lands in the global window options, where show -gw mode-keys finds it. Explicit -wg says the same thing without the sleight of hand.

And that is the load-bearing rule, and it is the opposite of what most tutorials say: an option with no value of its own falls back to the global layer at the moment it is read — live, not copied at creation. Your own earlier evidence already said so: the prefix flipped on a running server mid-flight, and every existing session obeyed. Two deliberate falsifications of the copy-at-creation story, both on a fresh throwaway — first base-index, where the copy story predicts a pre-change session can never see the new value:

$ tmux -L t7fix -f /dev/null new-session -d -s old '/bin/zsh -f'   # born under base-index 0
$ tmux -L t7fix new-window -d -t old -n w2
$ tmux -L t7fix move-window -s old:1 -t old:2     # park w2 at index 2
$ tmux -L t7fix kill-window -t old:0              # index 0 is now free
$ tmux -L t7fix list-windows -t old -F '#{window_index}: #{window_name}'
2: w2
$ tmux -L t7fix set -g base-index 1               # the global changes NOW
$ tmux -L t7fix new-window -d -t old -n probe     # …in the same old session
$ tmux -L t7fix list-windows -t old -F '#{window_index}: #{window_name}'
1: probe
2: w2

If the session had copied base-index 0 at its birth, the new window would take the free index 0. It took 1 — the live global. Second, remain-on-exit, a window option, where the copy story says a window created before the change keeps the old behavior:

$ tmux -L t7fix new-window -d -t old -n pre '/bin/zsh -f'   # window born while global says…
$ tmux -L t7fix show-options -gw remain-on-exit
remain-on-exit off
$ tmux -L t7fix set -wg remain-on-exit on                   # …then the global flips
$ tmux -L t7fix send-keys -t old:pre 'exit' Enter; sleep 1
$ tmux -L t7fix display-message -p -t old:pre 'dead=#{pane_dead} cmd=#{pane_current_command}'
dead=1 cmd=zsh

Both from fresh -L t7fix captures, 2026-08-23 (socket reused across two server lives, each killed after; the # comments are annotations, the outputs verbatim). The pane-exit decision consulted the current global — dead=1 is §6's surviving dead pane, not a destroyed window.

So what does stick? The materialized effects of old values, not the values: window indexes already allocated, scrollback already trimmed to an old history-limit, borders already drawn. The original demonstration — one option, sessions on both sides of the change — was real, but it was showing materialization, not copying:

$ tmux -L lab set -g base-index 1          # the global changes now
$ tmux -L lab list-windows -t x -F '#{window_index}: #{window_name}'   # x is older
0: zsh
1: clean2
2: clean3
$ tmux -L lab new-session -d -s newidx    # created AFTER the change
$ tmux -L lab list-windows -t newidx -F '#{window_index}: #{window_name}'
1: kernel_task

Real capture, 2026-08-23. Window indexes are allocated when a window is created, reading whatever the global says at that instant — so session x's 0-based windows are frozen as data, and the session born after numbers its first window from the live global (1). Config files set -g because it is the fallback every valueless object reads, present or future; reloading still never renumbers existing windows, because their indexes are already materialized — that is what move-window -r and renumber-windows are for.

The opinionated build, one line at a time

What follows is a small config with opinions, argued honestly — including the cases where the stock default is the right call and the famous advice is wrong for you. Adopt it line by line or stop where your taste diverges; the point of showing the reasoning is that you leave able to argue your own config line by line.

Line 1–3 · The prefix: C-a, and the honest debate

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

The case for remapping: Ctrl+a sits under a finger on the home row, decades of GNU screen users arrive with the reflex, and Ctrl+b's alleged horror is overstated — a week in, nobody notices. The case against, which most tutorials skip: Ctrl+a is already beginning-of-line in every readline and zsh line editor — the key you press dozens of times a day. Inside tmux, that key now belongs to the prefix, and getting a literal Ctrl+a to your shell takes the double-tap: Ctrl+a Ctrl+a. That is what the third line buys — bind C-a send-prefix makes pressing the prefix again send a real Ctrl+a through to the pane. You keep the function; you pay one extra keystroke and one new habit. Screen migrants already have the habit. If you have never run screen, staying on Ctrl+b is a completely defensible answer — it collides with almost nothing, and the guide works identically either way.

Line 4–5 · Splits: mnemonic keys, current directory

bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"

Two changes in two lines. The keys: Prefix | and Prefix - draw the split they make — no more "which of % and " was horizontal?" (§7's everyone-misremembers-it-once). The honest cost, read straight off the live binding table: - was delete-buffer (§9) and is now taken — prefix # and prefix = still reach the buffer stack, so nothing is lost, but a chord you knew has moved. The -c "#{pane_current_path}" is the quieter improvement: without it, a new split starts in the directory the server was started in (usually your home); with it, the split inherits the current pane's working directory — the directory you are obviously working in. #{pane_current_path} is a format (§12), the same language as every -F listing in this guide.

Line 6 · Reload, bound

bind r source-file ~/.tmux.conf \; display-message "config reloaded"

\; separates two commands in one binding — reload, then confirm the reload happened, because source-file is silent even when it works. The cost is another displaced chord: Prefix r was refresh-client (§8's force-redraw) and is now replaced outright — nothing moves it anywhere; if you want both, bind refresh to another key first. The smoke test of the assembled file confirmed the rebinding live: after one source-file, list-keys -T prefix showed r on source-file ~/.tmux.conf \; display-message "config reloaded".

Line 7 · Mouse on

set -g mouse on

Stock is off. With it on, the terminal's mouse reaches into tmux: click to select a pane, drag a divider to resize, wheel to scroll (into copy-mode — §9), drag text to copy. The honest cons, both real: tmux's mouse selection and your terminal's native selection fight — hold Shift (or Fn, per terminal) to let the terminal win when you want its clipboard behavior; and anything the mouse can do, this guide has taught a chord for, so treat the mouse as an accelerator, not a replacement for the keyboard.

Line 8 · vi keys, pinned

set -g mode-keys vi

You know the dependency from §9: the default follows EDITOR/VISUAL, so the same machine can hand you different tables depending on which shell started the server. Pinning it makes copy-mode predictable everywhere — and if you keep vim habits, remember y is still unbound until you bind it (§9's misconception card has the one-liner).

Line 9 · escape-time, kept low

set -s escape-time 10

Terminals send Esc slowly — §11 does this option full justice; here it is enough to know this is the milliseconds tmux waits to tell a lone Esc apart from the start of an arrow key, that too high a value measurably delays every Esc you press in vim, and that 10 — the stock default on 3.7b — is already right. This line exists to keep it right on machines whose older defaults were 500. One scope note: escape-time is a server option, so the block sets it with -s — matching the scope table above, and matching §9's set -s copy-command.

Line 10–13 · Sizes and cadence

set -g history-limit 50000
set -g status-interval 5
set -g base-index 1
set -g renumber-windows on

Four one-liners with one honest sentence each. history-limit is per-pane scrollback (stock 2000 lines) — 50000 costs RAM per pane, real but cheap, and only matters for panes that actually flood. status-interval is how often the status line re-runs its format strings (stock 15 s) — 5 makes clock and custom segments feel live (§14). base-index 1 starts numbering at one, so your first window lives on the Prefix 1 key instead of Prefix 0 — keyboards and humans both count from one; pure taste, zero function. renumber-windows on closes the gaps §6 demonstrated — windows compact after every close, so Prefix 2 is always "second window from the left" rather than "whatever survived." Both are read live at the moment they matter — base-index when a window is allocated (in any session, not just new ones), renumber-windows when a window closes. The second was verified directly: turning it on globally slid an existing session's windows 1,2,3 down to 1,2 the moment window 1 died — no new session involved. What never moves retroactively is already-allocated data: the indexes your windows hold today.

The whole file, assembled

# ~/.tmux.conf — the guide's opinionated build, assembled (section 10)
# Every line is explained above; this block is the whole file.

# Prefix: C-a instead of C-b, double-tap sends it through
unbind C-b
set -g prefix C-a
bind C-a send-prefix

# Splits: mnemonic keys, opening in the current pane's directory
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"

# Reload without restarting
bind r source-file ~/.tmux.conf \; display-message "config reloaded"

# Options
set -g mouse on
set -g mode-keys vi
set -s escape-time 10
set -g history-limit 50000
set -g status-interval 5
set -g base-index 1
set -g renumber-windows on

This exact block was smoke-tested while this guide was written: piped to a file, then tmux -L cfgtest -f <file> new-session -d — exit 0, empty stderr, and every option verifiably set on the throwaway server (prefix C-a, mouse on, mode-keys vi, escape-time 10, history-limit 50000, status-interval 5, base-index 1, renumber-windows on). Lab 8 makes you run the same test.

Lab 8 · Adopt, reload, revert
8 minutes · local · throwaway

The full life cycle of a config on a throwaway server: adopt it without restarting anything, watch each behavior change, revert to stock the honest way, then re-adopt. One scratch file, two terminals, zero risk to your everyday server — every command below carries -L lab.

1. Save the assembled block to a scratch file (the copy button above works, or paste it into your editor):

vim ~/.tmux-lab8.conf       # or: your editor of choice

2. Start stock and prove it is stock:

$ tmux -L lab -f /dev/null new-session -d -s cfg
$ tmux -L lab show-options -gv prefix
C-b

Expect: C-b — the factory prefix, on the record before you change it.

3. Adopt without restarting:

$ tmux -L lab source-file ~/.tmux-lab8.conf
$ tmux -L lab show-options -gv prefix
C-a
$ tmux -L lab show-options -gv mouse
on

Expect: C-a and on — the same mid-flight adoption captured live for this section. No session died, nothing restarted; the script simply ran against a live server.

4. Feel it from the inside. Attach with tmux -L lab attach -t cfg, then: Ctrl+a | (a vertical-line split, in your current directory), Ctrl+a r (the reload chord — "config reloaded" flashes in the status line), and check the split's directory from the second terminal:

$ tmux -L lab display-message -p -t cfg:0.1 '#{pane_current_path}'

Expect: your current working directory, not your home — #{pane_current_path} doing what the stock split does not. And the flash on r is display-message, which stays visible for display-time (stock: 750 ms) before fading.

5. Prove reload is not reset:

$ tmux -L lab source-file /dev/null
$ tmux -L lab show-options -gv prefix
C-a

Expect: still C-a. Sourcing an empty file ran zero commands; the earlier ones stand. If a config change ever "won't take," this is usually why — something older is still in force, and only an explicit set-back or a fresh server clears it.

6. See allocation read the live global, then revert the honest way:

$ tmux -L lab new-session -d -s fresh
$ tmux -L lab list-windows -t fresh -F '#{window_index}: #{window_name}'
1: zsh
$ tmux -L lab kill-server
$ tmux -L lab -f /dev/null new-session -d -s stock
$ tmux -L lab show-options -gv prefix
C-b

Expect: the new session's first window at index 1 — allocated while the live global said 1, the materialized side of this section's rule, now in your hands (the window's name is automatic renaming's business and may briefly read whatever process it caught at birth; this guide's live capture printed 1: kernel_task, yours will likely say zsh — the index is the assertion). Then — after the kill and a fresh -f /dev/null start — C-b again: genuinely stock, because a new server fell back to factory defaults. Re-adopt with one source-file ~/.tmux-lab8.conf to complete the loop: stock → adopt → reload → revert → adopt.

Checkpoint:

  • The prefix flipped on a live server, and you can explain why no restart was needed (the config is a script; source-file runs it)
  • The empty source-file changed nothing — and you can say what would have undone the prefix instead
  • A session created after base-index 1 started at window 1 while the older session kept its 0 — allocation reads the live global; the older session's indexes were already materialized
  • You know which two stock chords the config displaced (- and r) and where that functionality still lives

Teardown: tmux -L lab kill-server, then rm ~/.tmux-lab8.conf if you do not want the scratch file. Adopting this config for real is a copy into ~/.tmux.conf and one reload — but that server is yours, so read the arguments first, not just the lines.

Sources: tmux(1) — set-option, show-options, source-file, bind-key; FILES; OPTIONS (option scopes and global inheritance, escape-time, history-limit, status-interval, base-index, renumber-windows) · transcripts: research captures 2026-08-22 (options dumps, defaults) and fresh -L t7/-L t7fix/-L cfgtest captures 2026-08-23 (scope fallthrough, live global fallback — base-index and remain-on-exit falsifications, renumber-on-close, materialized indexes, adopt-without-restart, reload-is-not-reset, assembled-config smoke test), throwaway servers only

11 · Key bindings & tables

Every keystroke tmux sees is looked up in a key table, and you already know three of them: the root table (keys without a prefix — mostly mouse events on 3.7b), the prefix table (every chord this guide has taught), and the copy-mode tables from §9. Bindings are data, not doctrine: list them, rebind them, stack commands, repeat them. This section is the machinery — and the two timer options, escape-time and repeat-time, that decide how tmux feels before it decides what a key meant.

bind, unbind, and the tables underneath

bind-key adds a binding to a table; unbind-key removes one; list-keys prints a table whole. The defaults were never secrets — they are the output of tmux list-keys -T prefix, which is where every "command underneath" cell in this guide's chord tables came from. A full custom binding round trip, from the research run:

$ tmux -L kb bind -T prefix T display-message 'bound'
$ tmux -L kb list-keys -T prefix | grep ' T '
bind-key    -T prefix T       display-message bound

The -T prefix names the table. Omit it and you also get the prefix table — that is the ordinary spelling, bind x …. New tables cost nothing: bind -T mine x … creates mine on first use, and the key-table session option says which table a client consults without a prefix pending (stock: root). That is how nested prefixes, modal setups, and §9's vi-style tables are built — a table is just a name, and which table is live is just state.

bind -n reaches into every program you run. bind -n C-l … puts a binding in the root table — no prefix, it fires on the bare key. That is the power: instant global hotkeys. That is also the hazard: the program in the pane never sees the key again. Ctrl+l stops clearing your shell's screen, stops redrawing vim — every pane, every session, everywhere on the server. A live round trip on 3.7b confirmed the mechanism in both directions: bind -n C-l display-message 'root hit' appeared in list-keys -T root, and unbind -n C-l removed it, restoring the key to your programs. The rule: root-table bindings should be keys nothing you run cares about — or keys you have decided tmux owns.

What lives in the stock root table on 3.7b is worth one look, because it is not what people expect: it is almost entirely mouse bindings. Wheel-up routes into copy-mode, clicks select and resize, and the right-click menu alone offers paste, search-for-word, copy-line, split, swap, kill, respawn, and zoom. There is barely a keyboard in it — the keyboard reaches tmux through the prefix, and the mouse through the root table. Two of those root bindings have already appeared in this guide: MouseDown1ScrollbarUpcopy-mode -u (§9) and MouseDrag1Panecopy-mode -M (Lab 7's drag-copy).

Repeat: bind -r and repeat-time

Some prefix chords are repeatable: press the prefix once, then tap the key again and again within a window of time. The shipped bindings carry the flag — straight off this machine's table, every navigation and resize chord has it:

bind-key -r -T prefix Up      select-pane -U
bind-key -r -T prefix Down    select-pane -D
bind-key -r -T prefix M-Up    resize-pane -U 5
bind-key -r -T prefix C-Down  resize-pane -D

The window is the repeat-time session option — 500 ms stock, verified live (show-options -gv repeat-time). In practice: Prefix walks three panes with one prefix. The keyboard resize chords from §7 are where most people first feel it. Your own bindings opt in with -r: bind -r H resize-pane -L 5 — and a binding that is not repeatable drops you back out of the prefix after one press, which is the difference between a resize chord that flows and one that fights you.

escape-time: why terminals send Esc slowly

The arrow key arrives at tmux as the three bytes ESC [ A. A lone Esc arrives as one byte — ESC — that is a prefix of the same sequence. The two are indistinguishable until either more bytes arrive or time passes, so tmux waits: the man page defines escape-time as "the time in milliseconds for which tmux waits after an escape is input to determine if it is part of a function or meta key sequences." Every standalone Esc you press is delayed by that wait — every time, by up to the full value.

Stock on this machine's 3.7b: 10 ms — verified live alongside repeat-time. That is imperceptible and correct for a modern terminal. Raise it and you can watch the delay appear, measured on a real rig: a pane timestamping its input received a bare Esc 1.200 s after it was sent with escape-time 1200. Anyone who has felt vim "lag on Esc" inside a badly configured tmux has felt this option — and the fix is the config from §10: ten milliseconds.

A measurement honesty note, because it bit the author. The same rig's numbers below 500 ms all read ≈ 0.500 s regardless of the configured value — not tmux behavior but a rig artifact: when the terminal has not answered tmux's startup capability queries, tmux raises its internal escape-delay floor to 500 ms (source, tty-keys.c, tag 3.7b). A fake pty terminal never answers; your real terminal does, and gets the configured delay. Two lessons: quote the 1200 → 1.200 s demonstration, not the floor; and if Esc ever lags by exactly half a second, the terminal's capability handshake — not your config — is the suspect.

What to memorize, what to look up

The prefix table on 3.7b has dozens of bindings; daily driving needs about twenty of them, and the rest are exactly as findable as they are forgettable. tmux ships its own lookup, and it is better than any cheat sheet because it cannot drift: Prefix ? lists the whole table, and Prefix / prompts for one key name and shows every table that binds it:

$ tmux list-keys z
bind-key  -T copy-mode-vi z send-keys -X scroll-middle
bind-key  -T prefix       z resize-pane -Z

That is the whole output for z, live: two lines, two tables, no paging. Prefix / runs exactly this. If you learn one meta-chord from this section, make it the one that finds the others.

The split, as this guide's sections taught it — chords worth owning cold versus chords to look up when needed:

Worth memorizingWhyLook up when neededWhat it is
d · s · $ · ( )session verbs — §5&kill window (with confirm)
c · n p · l · 09 · w · ,window verbs — §6' · .jump to index · move window to index
% · " · o · ; · z · x · !pane verbs — §7* · Spacenew pane (3.7 series) · next layout
[ · ] · = · #copy verbs — §9- · M-n/M-pdrop top buffer · next/previous alert
? · /the lookups themselvesq · C-zflash pane indexes · suspend client

Every command underneath these chords is in the per-section tables of §5§9, and the cheat sheet (§17) repeats them dense. The memorize column is roughly the simulator's (the Prefix playground after §7) keymap — deliberately so.

Sources: tmux(1) — bind-key, unbind-key, list-keys, key-table, escape-time, repeat-time; DEFAULT KEY BINDINGS · tmux source, tag 3.7b — tty-keys.c (escape-delay floor), accessed 2026-08-22 · transcripts: research captures 2026-08-22 (prefix and root tables, custom-binding round trip, escape-time measurements) and fresh -L t7 captures 2026-08-23 (repeatable bindings, bind -n round trip, list-keys z lookup, option defaults), throwaway servers only

12 · Formats & scripting

Every listing in this guide — #{window_index}: #{window_name}, #{pane_id}, #{client_session} — has been written in a small template language you have been reading for eleven sections without its name. It is called a format, variables spell #{name}, and this is the section where it becomes yours. Once the server's entire object tree renders as plain text on demand, tmux stops being an application you operate and becomes a database you can query — and script. The section ends with the payoff built and proven live: tss, a ten-line fzf session jumper that turns "which session was that" into one chord.

The format language, and its printf

A format is a template the server evaluates against a target object. display-message -p is its printf: -p prints the expanded template to stdout instead of flashing it in the status line, and -t names the object the variables read from. You have run this a dozen times already; once, on purpose, with four variables at once:

$ tmux -L t8 display-message -p -t alpha:0.0 'session=#{session_name} window=#{window_name} cmd=#{pane_current_command} path=#{pane_current_path}'
session=alpha window=zsh cmd=zsh path=/private/tmp

Fresh capture, 2026-08-23, on this section's throwaway -L t8 server: three sessions (alpha, beta, gamma), alpha carrying a second window split into two panes. Every transcript below is from that server or, where noted, the research rig.

Context is the whole game: the same template against a different target makes a different true statement. Aim it at a pane and #{session_name} names that pane's session; aim it at a client and #{client_session} names the session that client is viewing. The workhorse variables, every one already met in this guide:

VariableHoldsFirst met
#{session_name}The session a target belongs to§5
#{window_index} / #{window_name}A window's position and label§6
#{window_flags}The flag string *-!#~ZM from the decoder§6
#{pane_id} / #{pane_index}A pane's identity (%N) and its position§7
#{pane_current_command} / #{pane_current_path}What runs in the pane, and where§10 (the -c split)
#{client_session} / #{client_termname}What a client views, through which $TERM§5 / §8
#{host_short}The server host's short namebelow, and §14

Conditionals, operators, and two traps

Formats are a language, not just substitution. The conditional is #{?cond,yes,no} — the listings in the next sub-section lean on it: #{?pane_active,[active], } prints [active] for the active pane and a space otherwise. Commas separate the conditional's parts, so a literal comma is written #, — a detail the stock status bar will make you meet again in §14. The operators, all live:

$ tmux -L t8 display-message -p 'and=#{&&:1,0} or=#{||:0,1} eq=#{==:cat,cat} match=#{m:c*t,cat} trunc=[#{=/3/...:abcdef}]'
and=0 or=1 eq=1 match=1 trunc=[]

Boolean and, boolean or, string equality, glob match — and a truncation that apparently did nothing. It did exactly what 3.7b says it does, which is nothing, and that is the first trap:

Drift: the truncation modifier works on variables, not literals. #{=N:var} keeps the first N cells of a variable — verified live: #{=2:session_name} on session gamma prints ga. Put a literal string in the same position and it expands to empty: #{=/3/...:abcdef} printed [] above. The elision suffix spelling #{=/N/...:var} is the same modifier with an ellipsis — it appears in the stock root-table bindings (#{=/9/...:buffer_sample}, §11). If a format silently empties, check whether you fed a modifier a literal.
Misconception: "format strings are shell strings." They are evaluated by the server, in tmux's grammar, before any shell is involved. Inside #{...} you write tmux's operators; outside it, whatever survives is shell. That is why every transcript in this guide single-quotes the whole format — '#{session_name}' — so your shell passes it through untouched — and why "$pick" in the script below is shell while '#{session_name}' next to it is tmux. One consequence for parsers: in format output a literal # arrives doubled — §6's flag probe printed flags=##~* for activity+silence+current — so emit ## when you want one, and expect two when you read.

The server as a database

The list- commands take -F and print your format once per object — the entire server, one query. The whole state of t8, twice:

$ tmux -L t8 list-panes -a -F '#{session_name}:#{window_index}.#{pane_index} #{pane_id} #{?pane_active,[active], }#{pane_current_command}'
alpha:0.0 %0 [active]zsh
alpha:1.0 %3  zsh
alpha:1.1 %4 [active]zsh
beta:0.0 %1 [active]zsh
gamma:0.0 %2 [active]zsh
$ tmux -L t8 list-windows -a -F '#{session_name}:#{window_index} #{window_name} #{?window_active,ACTIVE,}'
alpha:0 zsh
alpha:1 work ACTIVE
beta:0 zsh ACTIVE
gamma:0 zsh ACTIVE

Read the pane listing the way a script would. [active] appears once per window, not once per server — pane_active marks the active pane of each window. The pane ids are allocation-ordered across the whole server: %1 is beta's first pane and %2 gamma's, both created before alpha's window-1 panes %3 and %4 — identity follows creation, not position (§7's lesson, now in data). And ACTIVE in the window listing marks each session's current window. Options and buffers answer the same way: show-options -gv name prints a value a script can test (§10), and show-buffer prints the top of the stack (§9) — the readable faces of the same introspection.

Drift: an unknown variable expands to nothing. Silently. The obvious-looking #{session_created_string} does not exist in 3.7b, and asking for it does not error — it just prints empty, both here and in the research run that caught it:
$ tmux -L t8 list-sessions -F '#{session_name} created=#{session_created_string}'
alpha created=
beta created=
gamma created=
$ tmux -L t8 list-sessions -F '#{session_name} created=#{session_created}'
alpha created=1787541489
beta created=1787541489
gamma created=1787541489
$ date -r $(tmux -L t8 display-message -p -t alpha:0.0 '#{session_created}')
Sun Aug 23 22:18:09 CDT 2026
The timestamp exists; the friendly pre-formatted spelling does not — convert externally with date -r. Same family, met in the research: #{client_colorterm} is not a format variable either (§14 hits it). When a variable prints empty, do not assume the value is empty — check the man page's FORMATS list before you trust the silence.

Target scripts at ids, not indexes

§7's rule — indexes are positions, ids are identities — is the difference between a script that works once and one that keeps working. Ids never move; indexes renumber under any structural change. A pane also knows its own id, which is how scripts running inside a pane address themselves:

$ tmux -L t8 display-message -p -t beta:0.0 'i-am=#{pane_id}'
i-am=%1
$ tmux -L t8 send-keys -t beta:0.0 'echo "pane knows itself: $TMUX_PANE"' Enter
$ tmux -L t8 capture-pane -p -t beta:0.0 | grep 'knows itself'
pane knows itself: %1

Two languages, one pane: #{pane_id} asks the server about the target; $TMUX_PANE asks the environment from inside it — and both said %1. Scripts and bindings should target %N; humans reading listings can use either.

Hooks: events, with receipts

A hook is a named event the server fires when something happens — a window created, a client detached, a pane focused. set-hook maps an event to a command; the command runs through run-shell, so it is a shell command with the full format language available. The receipt, because hooks are invisible when they work:

$ tmux -L t8 set-hook -g after-new-window 'run-shell "echo NEWWINDOW >> /tmp/t8-hook.log"'
$ tmux -L t8 new-window -d -t gamma -n hookbait
$ sleep 0.6 && cat /tmp/t8-hook.log
NEWWINDOW
$ tmux -L t8 show-hooks -g | grep -c .
57
$ tmux -L t8 show-hooks -g | grep after-new-window
after-new-window[0] run-shell "echo NEWWINDOW >> /tmp/t8-hook.log"

No client was attached to t8 at any point in that transcript — after-* hooks fire unconditionally, and run-shell works detached, which is what makes hooks scriptable infrastructure rather than UI sugar. -g installs the hook server-wide (every session); without it, the hook lands on the current or targeted session. show-hooks -g prints the whole catalog — 57 hook names on 3.7b — with unset hooks as bare names and your hooks carrying their commands. The [0] is the hook's array index: hooks are array options, and the man page says setting one without specifying an index "clears the hook and sets the first member of the array" — which is exactly what show-hooks renders. One catalog entry has a condition worth knowing before you rely on it: pane-focus-in fires only when the focus-events option is on, and stock is off — captured in the research run, along with the man page's one-line explanation. set -g focus-events on first, or hook the after-* events, which have no such dependency.

pipe-pane: a pane, recorded honestly raw

pipe-pane connects a pane's output stream to a shell command — a tee, running for the pane's life or until you stop it. Logging is the obvious use:

$ tmux -L t8 new-window -d -t gamma -n pp '/bin/zsh -f'
$ tmux -L t8 pipe-pane -t gamma:pp.0 'cat >> /tmp/t8-pipe.log'
$ tmux -L t8 send-keys -t gamma:pp.0 'echo PIPE-TEST-LINE' Enter
$ cat -v /tmp/t8-pipe.log | tail -3
e^Hecho PIPE-TEST-LINE^[[?2004l^M^M
PIPE-TEST-LINE^M
^[[1m^[[7m%^[[27m^[[1m^[[0m                    …                    ^M ^M^M^[[0m^[[27m^[[24m^[[Jmacbook% ^[[K^[[?2004h
$ tmux -L t8 pipe-pane -t gamma:pp.0
$ echo "stop rc=$?"
stop rc=0

Long runs of spaces elided for width; everything else verbatim, including the leading e that the shell echoed and backspaced. Stopping is the same command with no argument — rc 0, the tee is detached, the pane never noticed. The pp window was then killed (kill-window -t gamma:pp), which is why the popup listing below counts the server's six panes.

Read what actually arrived: the raw stream. Bracketed-paste toggles (^[[?2004l/h), prompt color codes, the redraw — everything the pane's program ever wrote. That is the feature and the chore in one: a pipe-pane log is a faithful recording, not clean text. Filtering on read: col -b keeps the visible words but leaves sequence fragments behind (this run's col -b output still showed ?2004l); a full CSI strip — perl -pe 's/\e\[[0-9;?]*[a-zA-Z]//g' — comes out clean. When you want clean text rather than a recording, that is capture-pane -p's job (§9). One research-drift footnote for drivers: a keystroke sent while a pane is mid-redraw can be eaten — the research run's first attempt printed cho hi — so drive settled panes.

The hook map: events go to hooks, bytes go to pipe-pane pane %N one pty · one process (§7) Everything a pane does reaches the server as one of two things: events a window was created, a pane focused, a client detached — 57 named hooks in the catalog bytes the output stream itself — text, escape sequences, redraws, everything the program writes hooks — event-driven set-hook -g after-new-window \ 'run-shell "cmd"' runs when the event fires; works detached — zero clients attached in the receipt run above pipe-pane — continuous pipe-pane -t %N 'cat >> log' tees the raw byte stream to a shell command; runs until the bare pipe-pane stops it raw — filter on read, or capture-pane side effect /tmp/t8-hook.log NEWWINDOW any shell command, formats in the recording /tmp/t8-pipe.log faithful, raw, forever (until the pane or you stop it) events bytes both live in the server, not the client · hooks fire without one · a stopped pipe is rc 0 and silent a popup is neither — an overlay on the client, invisible to list-panes (below)
The hook map. Two taps on a pane's life: events flow to hooks, each firing a shell command through run-shell whether or not any client exists; bytes flow to pipe-pane's tee until you stop it. Popups, covered next, are a different animal entirely. Later sections refer back here as the hook map (§12).

display-popup: an overlay, with three honest limits

A popup is a rectangular overlay the client draws over the session, running a command — tmux's native floating window, and the natural launcher for pickers. It is also where scripting meets the client/server line, and the line pushes back three times:

  • It needs a live client. A popup is drawn on a client, so with none attached the command refuses — no current client, exit 1, straight from the research run's detached attempt. Same family as §5's switch-client refusal.
  • The CLI form blocks. tmux display-popup … from a shell holds the calling command for at least the popup's lifetime — the research run watched one still blocked ten seconds after a three-second popup command had exited. Popups are for bindings, which return instantly.
  • Without -E, it outlives its command. The man page: -E closes the popup automatically when the command exits (two -E: only if it exited successfully); -k lets any key dismiss it, not just Escape and Ctrl+c. Forget -E and your echo popup sits there, output and all, until you dismiss it by hand.

The binding form, driven live on a real attached client through a fresh pty rig in the research's style, shows everything else worth knowing:

$ tmux -L t8 bind -T prefix P display-popup -E -T 'popup demo' -h 40% -w 60% 'echo POPUP-BODY-TEXT; sleep 4'
### chord Prefix P pressed; the client's terminal received (ANSI stripped, spaces elided):
"/tmp" 22:21 23-Aug-26┌─popup demo──────────────────────────…──┐││││…
─────────────────────────────…──────────────────────────────────┘POPUP-BODY-TEXT
### list-panes -a while the popup is open:
alpha:0.0 %0
alpha:1.0 %3
alpha:1.1 %4
beta:0.0 %1
gamma:0.0 %2
gamma:1.0 %5
### Escape pressed: popup gone, client unharmed, still attached

The title renders inside the rounded top border after ┌─, the command's output lands inside the box — and list-panes -a shows six panes, none of them the popup. A popup is an overlay on the client: it is not a pane, not addressable by -t, invisible to every list- command, and it dies with the client that drew it. Scripts cannot drive a popup from outside; keystrokes reach it only through the client. One research footnote with a smile: -B means no border, and -T is the title — the research author initially passed -B 'title', tmux took the string in stride as the command, and the error text rendered inside the popup, which is itself a neat demonstration of both.

The worked example: tss, a session jumper

Everything above assembles into something you will use daily. The ingredients: list-sessions -F turns the server into a list (§12's database); fzf turns a list into a picker; switch-client is §5's pointer-mover — instant, no detach — with its known sharp edge: it needs a current client. The script, in full:

#!/bin/sh
# tss — tmux session jumper: pick a session with fzf, switch to it.
# Inside tmux: switches the current client. Outside (or detached): prints
# the attach command, because switch-client needs a current client.
#
# ~/bin/tss · section 12 of the tmux user guide · requires: tmux, fzf

pick=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | fzf) || exit 0
[ -n "$pick" ] || exit 0

if tmux switch-client -t "$pick" 2>/dev/null; then
    exit 0
fi
echo "no current client — attach instead:"
echo "    tmux attach -t '$pick'"

Four design decisions, each traceable to this guide. The 2>/dev/null on list-sessions: with no server running, the error goes to stderr and fzf simply gets an empty list — the script stays quiet instead of alarming. The || exit 0: fzf exits nonzero when you cancel with Escape, and a cancelled pick should do exactly nothing. The guard: switch-client is attempted, and if it fails — no current client — the script prints the attach command instead of failing silently, converting §5's sharp edge into a helpful message. And the bare tmux throughout: inside any pane, the $TMUX environment variable points the command at the pane's own server, so the script needs no socket logic. (For §4's named-server friction, prefix both tmux calls with -L name — a one-line change.)

Run live, through a real attached client on the t8 server — the transcript the section promised:

### client attached; the server says:
/dev/ttys027 alpha
### ~/bin/tss run inside the pane; the pane now shows fzf:
26: ▌ gamma
27: ▌ beta
28: ▌ alpha
29:   3/3 ──────────────────────────────…──────
30: >
### typed gam, pressed Enter; the server says:
/dev/ttys027 gamma
### the pane tss ran in — still in alpha, prompt back:
6: ❯ ~/bin/tss
9: ❯

capture-pane line numbers; the rule line's long dash run elided for width; line 8 is this machine's prompt styling, skipped. One chord's worth of typing moved the client from alpha to gamma — the pane that ran the script stayed in alpha, its shell back at the prompt, exactly the pointer-versus-process distinction §5 taught.

The guard, same server, zero clients attached — the script driven into a detached pane with send-keys:

$ tmux -L t8 list-clients
(no rows — server alive, no clients: §8's table)
$ tmux -L t8 send-keys -t alpha:0.0 '~/bin/tss' Enter
$ tmux -L t8 send-keys -t alpha:0.0 -l bet && tmux -L t8 send-keys -t alpha:0.0 Enter
$ tmux -L t8 capture-pane -p -t alpha:0.0 | grep -n 'no current' -A1
10:no current client — attach instead:
11:    tmux attach -t 'beta'

The numbers are capture-pane row numbers, via grep -n — same convention as the main tss transcript.

And the form that ties the section together — the popup as the tmux-native launcher, fzf and all, one binding:

$ tmux -L t8 bind -T prefix P display-popup -E -h 60% -w 60% '~/bin/tss'
### real client, Prefix P, typed bet, Enter; the server says:
/dev/ttys027 beta

The popup opened running tss, fzf filled it, one selection later the client had switched and -E closed the popup behind the switch. That binding is the whole "plugin" people install managers for — in one line, with every part visible.

Lab 9 · Build tss
12 minutes · local · needs fzf · throwaway

The script above, built with your hands, run against three real sessions. ~/bin is used because it is the standard personal-binary directory and it is already on this machine's PATH — check yours in step 2 rather than trusting that.

1. Check the precondition:

$ command -v fzf
/usr/local/bin/fzf
$ fzf --version
0.74.2 (Homebrew)

Expect: both lines — the values are this machine's; any recent fzf behaves the same for this lab. If command -v fzf prints nothing, install it first (brew install fzf on macOS) — fzf is the picker, and there is no fallback for it here.

2. Make sure ~/bin exists and is on your PATH:

mkdir -p ~/bin
echo "$PATH" | grep -q "$HOME/bin" && echo "on PATH" || echo 'off PATH — add: export PATH="$HOME/bin:$PATH"'

Expect: on PATH (if not, add the export to your shell's rc in a new terminal, or use the full ~/bin/tss path in the steps below).

3. Write the script exactly as the section shows it into ~/bin/tss, then:

chmod +x ~/bin/tss
command -v tss

Expect: the path to tss — your shell found an executable by that name.

4. Three sessions on the throwaway:

tmux -L lab -f /dev/null new-session -d -s project
tmux -L lab new-session -d -s scratch
tmux -L lab new-session -d -s monitor

Expect: silence, three times — and tmux -L lab ls lists project, scratch, and monitor for the picker to find.

5. Attach and jump: tmux -L lab attach -t scratch, then run tss at the pane's prompt, type mon, press Enter.

Expect: fzf lists the three sessions, filters to monitor as you type, and Enter lands you there — the status line's session list confirms it. From a second terminal: tmux -L lab list-clients -F '#{client_session}' prints monitor. Note the bare tmux inside the script worked against the lab server: the pane's $TMUX pointed it there.

6. The cancel path: run tss again, press Escape.

Expect: fzf closes and nothing else happens — || exit 0 doing its job. A picker you cannot cancel cleanly is a picker you stop using.

7. The guard: detach with Prefix d, then from your plain shell drive the now-detached pane:

tmux -L lab send-keys -t scratch:0.0 'tss' Enter
tmux -L lab send-keys -t scratch:0.0 -l monitor && tmux -L lab send-keys -t scratch:0.0 Enter
tmux -L lab capture-pane -p -t scratch:0.0 | grep -n 'no current' -A1

Expect: the two fallback lines — no current client — attach instead: and tmux attach -t 'monitor' — the same receipt as the section's. One honest footnote: the printed command is right from inside a pane, where bare tmux means this server; from your plain shell on a -L lab socket, run it with the flag: tmux -L lab attach -t monitor.

8. Optional, recommended: bind the popup launcher on this server and use it once — Prefix P, type, Enter:

tmux -L lab bind -T prefix P display-popup -E -h 60% -w 60% '~/bin/tss'
tmux -L lab attach -t project

Expect: fzf in a floating box over the session; one selection switches the client and closes the box behind you.

Checkpoint:

  • You jumped scratchmonitor with one fzf pick, and list-clients agreed
  • Escape cancelled cleanly — and you can name the script line that made it so
  • The guard fired with no client attached and printed the attach command — and you can say why switch-client refused (§5's sharp edge)
  • You can point at each of the script's four design decisions and the section that earned it

Teardown: tmux -L lab kill-server. Keep ~/bin/tss if you want it on your real server — it is read-only over the session list and switches only the client you run it from; remove with rm ~/bin/tss otherwise.

Sources: tmux(1) — FORMATS; display-message, list-panes/list-windows/list-sessions (-F), run-shell, set-hook, show-hooks, pipe-pane, display-popup, switch-client; HOOKS · junegunn/fzf (picker; exit codes, version), accessed 2026-08-23 · transcripts: research captures 2026-08-22 (detached popup refusal, CLI-blocking popup, -E man text, focus-events condition, pane-focus-in) and fresh -L t8 captures 2026-08-23 (everything else: formats, expressions, listings, created drift, ids, hook receipt, pipe log, popup render, tss main/guard/popup runs via a pty rig in the research style), throwaway servers only · rig and scratch removed after capture; the user's own servers untouched

13 · Plugins & ecosystem

tmux has no plugin system. What it has is a config file that is a script (§10) and a command that runs shell commands (run-shell, §12). A "plugin" is the natural consequence of those two facts: a git repository whose file your server executes, plus a tiny manager that keeps those repositories cloned and current. Understanding that sentence is most of this section. The rest is a tour of the plugins worth knowing — each with one signature behavior and one honest tradeoff — and the decision rule that keeps your config yours.

Evidence, labeled: this section is docs-verified only. Nothing here was installed or run on this machine — not TPM, not any plugin. What was done during research: TPM was shallow-cloned to a scratch directory and its code read (option names, the plugin-loading script, path defaults — the code-level claims below), and every plugin's README was fetched and read. Clone and scratch deleted afterwards. Where this guide live-tested the same capability itself, the text says so and points at the transcript; the plugin wrappers themselves remain unverified-on-this-box by design. Treat this section as a reading of the documentation, in the guide's usual voice.

What a plugin is, mechanically

A plugin repository ships a *.tmux script. TPM's loader executes each installed plugin's script — that is the entire mechanism. The script runs bind-key and set-option commands against your server, exactly as your own config does; from the server's point of view there is no difference between a plugin and forty lines you pasted. Which is why everything in §11 and §12 applies to debugging plugins too: list-keys and show-options show you what any plugin actually did.

The manager's shape, per TPM's README (with the code-level facts verified in the clone):

# ~/.tmux.conf — plugins via TPM (docs-verified; not run in this guide)
set -g @plugin 'tmux-plugins/tpm'          # the manager itself, listed first
set -g @plugin 'tmux-plugins/tmux-resurrect'
# …more @plugin lines, one per plugin…

run '~/.tmux/plugins/tpm/tpm'               # MUST be the last line
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm   # one-time install

Three chords run the manager: Prefix I installs (Install), Prefix U updates, Prefix M-u removes — all three rebindable via the @tpm-install/@tpm-update/@tpm-clean options read straight out of TPM's own scripts. Plugins land under ~/.tmux/plugins/ by default (XDG config location if your conf lives there, overridable by TMUX_PLUGIN_MANAGER_PATH). And the keep-at-bottom rule has a mechanical reason, not an aesthetic one: when the run line executes, TPM reads the @plugin options set above it and sources each plugin — that moment is the plugins' birth. Config placed after the line still runs — the file is a script — but it runs after the plugins loaded, so it cannot affect their setup, and any of their bindings have already had their chance to collide with yours. Your own overrides, if a plugin takes a chord you insist on keeping, belong below the run line; everything else belongs above it.

The tour: one behavior, one tradeoff each

PluginSignature behaviorThe honest tradeoff
tmux-resurrect Prefix Ctrl+s saves the whole environment — sessions, windows, panes, layout, cwd, active session/window, plus a conservative program list (vi vim nvim emacs man less more tail top htop …); Prefix Ctrl+r restores it after a restart or reboot. It saves structure, not pane contents — scrollback is gone, and programs restart fresh from the saved list. Snapshots are text files on your disk: readable, diffable, and something to clean up.
tmux-continuum Every 15 minutes, silently runs resurrect's save; with set -g @continuum-restore 'on', restores automatically when the server starts. Can auto-start tmux at boot. Depends on tmux-resurrect and bash, and it writes snapshots in the background forever — exactly the kind of always-on process you should know you have. Reboot-resilience is its whole point: servers are not daemons (§4), and continuum is the userland answer.
tmux-fzf Prefix F opens an fzf menu over sessions, windows, panes, commands, keybindings, clipboard, and processes. Requires GNU bash, sed, and fzf, and tmux 3.2+ for popup support. And notice what it is: for the session-jumping slice, §12's tss is the same feature in ten lines you own. The breadth is the product; the dependency chain is the price.
tmux-yank Restores vim muscle memory in copy-mode: y copies the selection to the system clipboard, Y copies and "puts" it on the command line. Reaches the clipboard through platform tools (macOS needs reattach-to-user-namespace on some setups; Linux xsel/xclip/wl-copy); its README does not claim OSC 52 at all — while §9 live-tested the built-in copy-command and OSC 52 routes on this machine. What yank adds is the familiar key and its line/word modes, not new plumbing.
catppuccin/tmux The pastel Catppuccin theme (Latte/Frappé/Macchiato/Mocha) for the status line, with configurable modules — cpu, ram, battery, uptime, session… A config surface comparable to hand-rolling §14's bar, plus a version floor (tmux 3.2+) and version-pinned installs ('catppuccin/tmux#v2.3.0'). Beautiful, popular, and one more thing between you and the status line when it misbehaves.
Oh My Tmux! Not a plugin — an entire opinionated config. Clone, symlink .tmux.conf, copy .tmux.conf.local; all your customization lives in that local file. Convenient and opaque. You adopt someone's status line, bindings, and opinions wholesale; when something misbehaves you are debugging a config you did not write. TPM integration is optional inside it. Everything it does, §10§14 taught you to do in ~25 lines you can defend line by line.

All rows from the projects' READMEs as fetched 2026-08-22 (URLs in Sources); tmux-resurrect's saved-state list and continuum's defaults verified against those READMEs; TPM's loader behavior verified against its code. None run here.

The decision rule: plugin, or five lines of config?

Adopt a plugin when the work is ongoing maintenance; write the five lines when the feature is one format string away. The two ends of the spectrum, both from this guide: tss is the session-jump feature of tmux-fzf in ten readable lines with zero dependencies — writing it beat installing it. tmux-resurrect is the opposite: a snapshot format that must track tmux's internal layout representation across versions, maintained by people who test against every release — that is genuine ongoing work, and adopting it is the right call. In between, the tie-breakers: does the plugin's key surface collide with chords you own (§11)? Would you debug it by reading its one file, or by filing an issue? And can you describe, in one sentence, what it changes about your server? If you cannot, you are not adopting a feature — you are adopting a mystery.

Reading a plugin's health before you trust it

The ecosystem's architecture makes this easy, and that is its quiet virtue: every plugin is a script you could read in five minutes, so "audit before install" is a real option, not a slogan. The checklist, in order of how fast they disqualify:

  • Last commit and open issues. A plugin that touches key bindings breaks with tmux releases; an unmaintained one is a future §16 entry.
  • Its version floor, versus yours. TPM supports tmux 1.9+; tmux-fzf wants 3.2+ for popup support, and catppuccin requires 3.2+. A README written against future-tmux is the same trap as §14's theme option: configs circulate faster than versions ship.
  • What it actually does. Read the *.tmux file. If what it binds and sets matches the README's claims, you have verified more than most users ever do — in five minutes, because there is nothing else to read.

Sources: all docs-verified (fetched 2026-08-22, none installed or run here) · tmux-plugins/tpm — README plus code read from a scratch clone (loader script, option names, paths) · tmux-plugins/tmux-resurrect — what is saved, keys · tmux-plugins/tmux-continuum — cadence, auto-restore · sainnhe/tmux-fzf — scope, requirements · tmux-plugins/tmux-yank — keys, platform tools, no OSC 52 claim · catppuccin/tmux — flavors, modules, version pinning · gpakosz/.tmux (Oh My Tmux!) — install shape, local-config rule · mechanism cross-references: this guide's §10, §12, §14

14 · Styling & status line

The status line is a format string — the language from §12 — rendered onto one row of your terminal on a timer. That is the entire secret. Copied dotfiles feel like magic only because nobody shows you the three option families underneath: what the bar says (templates), how it is drawn (styles), and what sits under all of it (the color and theme story, which on 3.7b has an honest ending). This section reads the stock bar line by line, then rebuilds it twice — a three-segment working bar, then a two-segment minimal one — each as a config you can paste, smoke-tested and rendered off a real client.

What the stock bar actually says

Every string below is a shipped default, read straight off a stock server in the research run:

$ for o in status-left status-right status-left-length status-right-length \
      window-status-format window-status-current-format status-style \
      status-justify status-position; do tmux -L st show-options -gv $o; done
[#{session_name}] 
#{?window_bigger,[#{window_offset_x}#,#{window_offset_y}] ,}"#{=21:pane_title}" %H:%M %d-%b-%y
10
40
#I:#W#{?window_flags,#{window_flags}, }
#I:#W#{?window_flags,#{window_flags}, }
bg=green,fg=black
left
bottom

Line by line, left to right. The left segment is the session name in brackets — the [alpha] you have read all guide — with a trailing space in the template, one cell of padding baked in: invisible in the bar, visible in the option. The right segment is a three-part template: a conditional offset marker that appears only when the window is larger than the client viewing it (#{?window_bigger,…,} — inside it, the comma between the two offsets is escaped as #,, the escaping §12 introduced), then the pane title truncated to 21 cells (#{=21:pane_title} — the truncation modifier, on a variable, as it requires), then a strftime time and date. Yes, the stock bar has been speaking §12 this whole time. The window list is a format pair — #I:#W plus the flags from §6's decoder — and the famous green is one style string: bg=green,fg=black.

That render, off a real client on this section's t8 server with the bar's options untouched (ANSI stripped): [alpha] 0:zsh- 1:work*"/tmp" 22:21 23-Aug-26 — brackets, flags, truncated pane title in quotes, time. Which leaves the two numbers most people never notice:

Drift: the stock left segment is capped at 10 cells. status-left-length ships at 10, and the cap is enforced without ceremony, as a long-named session on a real client shows:
$ tmux -L t8 new-session -d -s averylongsessionname -x 120 -y 30
### real client attached; status line as its terminal received it (ANSI stripped):
[averylong0:zsh*"~/guides" 22:23 23-Aug-26
[averylong — exactly ten cells, name amputated mid-word, closing bracket gone. Nothing errors; the bar simply lies by omission. Any custom left segment longer than a word needs status-left-length raised (the right side ships at 40 and needs the same attention).

The styles family, and the conditional you already know

The next defaults batch, same research dump — the styles that draw everything around the bar:

window-status-style => default
window-status-current-style => default
mode-style => noattr,bg=yellow,fg=black
pane-border-style => default
pane-active-border-style => #{?pane_in_mode,fg=yellow,#{?synchronize-panes,fg=red,fg=green}}
pane-border-status => off
default-terminal => tmux-256color

The one worth staring at is the active border — a live conditional, nested two deep: green normally, yellow when the pane is in a mode (that is §9's copy-mode tell, and it is not hardcoded anywhere — it is this option), red while synchronize-panes is on (§7's danger state, telegraphed by the border before you type). A style value can be a full format, which means everything §12 taught applies to paint as well as text. The syntax itself is one line: fg= and bg= take a named colour (green), a 256-colour index (colour121), or a hex triple (#ff8800); attributes (bold, noattr, …) ride along comma-separated; default means "inherit." Which colours you may actually send is the next question.

256 colours, true colour, and what your client already negotiated

The $TERM contract from §2 decides what the client's terminal can receive, and tmux resolves it into a feature list per client. What flips the RGB feature on was re-verified for this section with a controlled pair — two real attach clients on one throwaway server, identical TERM=xterm-256color, differing only in environment, receipts first:

### receipts: each attach client's pid, its tty, and its COLORTERM (ps):
pid 77319 on ttys013: COLORTERM=truecolor
pid 77320 on ttys014: (no COLORTERM)
$ tmux -L t8fix list-clients -F '#{client_name} features=[#{client_termfeatures}] COLORTERM=#{client_colorterm}'
/dev/ttys013 features=[bpaste,ccolour,clipboard,cstyle,focus,RGB,title] COLORTERM=
/dev/ttys014 features=[bpaste,ccolour,clipboard,cstyle,focus,title] COLORTERM=

Read the two rows against the receipts: the client on ttys013, carrying COLORTERM=truecolor, resolved RGB; the identical client on ttys014, without it, did not — stably, in a second run that probed at 3 and again at 8 seconds, so it is not a query-timeout race. On these clients the gate is the environment variable, not the TERM name alone. In practice that is still "zero configuration": every modern terminal exports COLORTERM=truecolor itself, so a normal client arrives pre-qualified, and the famous terminal-overrides *:RGB dance (or the older Tc spelling) remains the fix for terminals that neither export it nor answer tmux's capability queries. Practical upshot for styling: hex triples reach clients that signal true colour, and 256-index colours work everywhere.

Drift: an earlier research capture disagrees with this one — and the receipts decide. The 2026-08-22 research run showed RGB on both of its two clients, one of which it described as lacking COLORTERM; this section's fresh run, with the environments proven per client via ps eww, could not reproduce that row — TERM=xterm-256color alone did not resolve RGB. When a capture without receipts and a controlled capture with them disagree, the guide teaches the one you can re-run. If your own terminal shows RGB without COLORTERM set, it answers tmux's startup queries — the other legitimate road in — and list-clients -F '#{client_termfeatures}' will tell you which side you are on.

And the trailing COLORTERM= in both rows — empty even for the client whose environment demonstrably carries it — is a face of the drift from §12: #{client_colorterm} is not a format variable on 3.7b. The feature list is the truth; no format reads the environment variable directly.

The window list: two slots and a separator

The window list is drawn from exactly two templates — window-status-format for ordinary windows, window-status-current-format for the current one (the only special-cased slot; everything else in the list is the same template repeated). Between entries goes window-status-separator, whose stock value is a single space — read off the live server: show-options -gv window-status-separator prints one blank-ish line, [ ] when wrapped in brackets to see it. Two posture options round out the family: status-justify left|centre|right moves the window list within the bar, status-position top|bottom moves the bar itself. And remember that #W in any of these templates tracks automatic renaming (§6) — the list re-labels as programs come and go, on the same terms as the stock bar.

The theme option: arriving in 3.8, absent here

Drift: there is no theme option on 3.7b. The research grepped a stock server's entire option space for it and found nothing (show-options -g | grep -i theme → exit 1, no matches), and handing it to set-option fails loudly:
$ tmux -L t8x set-option -g theme dark
invalid option: theme
$ echo "rc=$?"
rc=1
The theme system is real but it belongs to the next release — tmux's CHANGES for 3.8 (master, accessed 2026-08-22) introduces built-in light and dark colour themes for 256+-colour terminals, a theme option (auto-detected from the terminal, taken from terminal colours, or forced), theme colour names usable in styles (themeblack, themegreen, …), colours in styles expanded as formats, and the terminal theme reported to panes. Version attribution is the whole point: when a dotfile on the internet shows a theme line, it is from the future, and on your 3.7b it is an error — the same "configs circulate faster than versions ship" trap as §13's version floors.

The rebuild, take one: three segments

Design first, lines second: a session chip on the left, the window list with flags in the middle, host and time on the right — dark base, one accent. Inline styles use #[…] — the # again, §12's language wearing a different hat — and #[default] resets to the segment's base style:

# status bar, take one: three segments — session · window list · time+host
set -g status-style 'fg=white,bg=black'
set -g status-left '#[fg=black,bg=cyan,bold] #S #[default]'
set -g status-left-length 30
set -g status-right '#[fg=cyan]#{host_short} #[fg=yellow]%H:%M #[default]'
set -g status-right-length 40
set -g window-status-format '#[fg=white]#I:#W#{?window_flags,#{window_flags}, }'
set -g window-status-current-format '#[fg=black,bg=cyan,bold]#I:#W#{?window_flags,#{window_flags},}#[default]'
set -g window-status-separator '  '

Three notes on the choices. #{host_short} was verified live before use (display-message -p 'host=#{host_short}'macbook) — the drift habit from §12, applied. The window templates reuse the stock #{?window_flags,…} logic verbatim, so the bar keeps §6's flag telegraphy under new paint. And status-left-length 30 exists because of the drift card above. Smoke-tested exactly like §10's config (block to a file, tmux -L cfg8 -f <file> new-session -d — exit 0, every option verified on the throwaway), then rendered by a real attached client:

### real client, 120 columns; bar as its terminal received it (ANSI stripped, spaces elided):
 demo  0:zsh- 1:work*  macbook 22:22

Colors do not survive a text capture — the current window 1:work* wears the cyan chip, host and time their tints; the text is the structure. Session chip, flag-annotated list, host and clock: three segments, every cell accounted for.

The rebuild, take two: minimal, two segments

The opposite posture: the bar says only where you are and what time it is, and the window list retires entirely — empty templates are legal, and the left segment can carry #W itself:

# status bar, take two: minimal — two segments, window list gone
set -g status-style 'fg=white,bg=black'
set -g status-left ' #S · #W '
set -g status-left-length 40
set -g status-right '%H:%M '
set -g status-right-length 12
set -g window-status-format ''
set -g window-status-current-format ''
set -g window-status-separator ''

Same smoke test, exit 0, options verified; same real client render:

### real client, 120 columns; as its terminal received it (ANSI stripped, spaces elided):
demo · work22:23 

With spaces elided the two segments run together — demo · work on the left, 22:23 at the far right edge, the bar's remaining width as silent padding between them. Minimal costs you the at-a-glance window ring; what it buys is a bar that never needs re-reading. If take-two-plus-flags is what you actually want, take one with window-status-format '' is three lines away.

The timer under the bar

One mechanism note to close the loop: the bar is re-evaluated on a timer — status-interval, stock 15 seconds — and on events that change what it shows. A %H:%M segment ticks with the timer; a #{pane_current_command} segment would update as programs change. §10's opinionated build set it to 5 for a livelier clock; if your bar runs heavy commands through #[…]-wrapped run-shell tricks, that timer is the cost you are tuning. When the whole bar-as-config surface feels like more than you want to own, §13's catppuccin row is the buy-versus-build version of this section — now you know exactly what you would be buying.

Sources: tmux(1) — status-left, status-right, status-left-length/-right-length, status-style, status-justify, status-position, status-interval, window-status-format, window-status-current-format, window-status-separator, window-status-style, mode-style, pane-border-style, pane-active-border-style; STYLES; FORMATS · tmux CHANGES, master — "CHANGES FROM 3.7c TO 3.8" (theme system, paraphrase-grade summary), accessed 2026-08-22 · transcripts: research captures 2026-08-22 (defaults dump, both option blocks, theme grep; the RGB features capture is superseded by the re-run below, per the drift card) and fresh -L t8/-L t8x/-L t8fix/-L cfg8 captures 2026-08-23 (long-name truncation render, separator default, host_short, theme-option error, both smoke tests and both bar renders via a pty client, and the controlled two-client RGB run with ps pid-to-tty environment receipts plus its 3 s/8 s stability re-run), throwaway servers only

15 · Capstone lab

One script, every module. projectx is tmuxinator from scratch — §13's decision rule said to write it yourself when the feature is twenty readable lines away, and these are the twenty lines: a named server, a guarded session, three windows built to spec with intended panes and starting directories, an fzf picker to get in, and a teardown that verifies itself. Build it in four phases, then run it end to end and watch the receipts from §4 through §12 come back in one sitting. 25 minutes · local · throwaway.

What you are building

The spec before any code — the draw-then-check habit from §6's labs, applied to a whole workspace:

WindowPanesStarts inFor
edit1$ROOT/srcthe editor
build2, side by side$ROOTbuild and tests, watching each other (§7's split)
docs1$ROOT/docsnotes, man pages, the README you owe

Three knobs live at the top of the script — SOCKET, SESSION, ROOT — each overridable from the environment. That one design decision is what makes the script testable: your real project runs on the work socket with ROOT=$HOME/src/projectx, and this lab's dry run points the same unedited script at a scratch root and a throwaway cap socket. It is §4's clean-room habit and §10's adopt-without-risk pattern, built into the tool itself.

Phase 1 · The named server and the guard

SOCKET="${PROJECTX_SOCKET:-work}"             # the named server (section 4)
SESSION="${PROJECTX_SESSION:-projectx}"       # the session this script builds
ROOT="${PROJECTX_ROOT:-$HOME/src/projectx}"   # where the project lives

create)
    # Guard: has-session is the existence probe.
    if tmux -L "$SOCKET" has-session -t "$SESSION" 2>/dev/null; then
        echo "$SESSION already exists on socket '$SOCKET' - nothing created."
        echo "re-enter it with: $0 attach"
        exit 0
    fi

Two pieces of doctrine in four lines. The server is named — every tmux call carries -L "$SOCKET", so one script builds its own world and its final kill-server can never reach anything you care about (§4). And the guard is has-session: exit 0 if the session exists, exit 1 if it does not, silent either way — an existence test your script checks by exit status, never by parsing output (the discipline §4 and §8 built their state tables on). For a single session, tmux new -A -s name gives you get-or-create in one flag (§17); a script that builds nine panes across three windows needs the long form — and the guard is it.

Expect: a fresh run prints created projectx on socket 'cap' - enter it with: … attach and exits 0. Run it again and the guard answers instead: projectx already exists on socket 'cap' - nothing created. — also exit 0, because refusing to rebuild is the success case. Both lines are quoted in the captured run below.

Phase 2 · Three windows, to spec

    # The directories the windows will start in.
    mkdir -p "$ROOT/src" "$ROOT/docs"

    # Window 1 "edit": one pane, in the source root.
    tmux -L "$SOCKET" new-session -d -s "$SESSION" -n edit -c "$ROOT/src"
    # Window 2 "build": two panes side by side (§7's split).
    tmux -L "$SOCKET" new-window -t "$SESSION" -n build -c "$ROOT"
    tmux -L "$SOCKET" split-window -h -t "$SESSION:build" -c "$ROOT"
    # Window 3 "docs": one pane, in the docs tree.
    tmux -L "$SOCKET" new-window -t "$SESSION" -n docs -c "$ROOT/docs"
    # Land on the editor.
    tmux -L "$SOCKET" select-window -t "$SESSION:edit"

Every flag was taught, and each is doing intended work. -d creates the session detached — it exists before any client arrives (§5). -n names the windows so automatic renaming never gets a vote (§6). -c gives each window — and each split — its starting directory: the same flag §10's opinionated split used with a format, here taking plain paths. And split-window -h deals build its two side-by-side panes (§7). The final select-window lands the session on the editor, so whoever attaches next starts in the right room.

Expect: the status subcommand's receipt — three rows, panes counted, flags honest:

$ tmux -L cap list-windows -t projectx -F '#{window_index}: #{window_name}  panes=#{window_panes} #{window_flags}'
0: edit  panes=1 *
1: build  panes=2
2: docs  panes=1 -

Expect: and the start directories, provable per pane — -c did what it claimed (#{pane_current_path}, §12's probe):

$ tmux -L cap list-panes -a -F '#{session_name}:#{window_index}.#{pane_index} cwd=#{pane_current_path}'
projectx:0.0 cwd=/private/tmp/pxroot/src
projectx:1.0 cwd=/private/tmp/pxroot
projectx:1.1 cwd=/private/tmp/pxroot
projectx:2.0 cwd=/private/tmp/pxroot/docs

Phase 3 · The picker

attach)
    # Section 12's tss pattern, one socket over: pick, then switch or attach.
    pick=$(tmux -L "$SOCKET" list-sessions -F '#{session_name}' 2>/dev/null | fzf) || exit 0
    [ -n "$pick" ] || exit 0
    if tmux -L "$SOCKET" switch-client -t "$pick" 2>/dev/null; then
        exit 0
    fi
    exec tmux -L "$SOCKET" attach -t "$pick"
    ;;

This is tss from §12 with two changes, each earned earlier in the guide. Every call carries -L "$SOCKET" — the named-server friction §4 promised §12 would script away, scripted away. And the fallback is exec attach rather than printing a suggestion: this picker is the script's front door, most often run from your plain shell, where attach is exactly right — the guard keeps the inside-tmux case working too (§5's sharp edge, §12's design). Cancelled fzf still exits 0 and does nothing, per || exit 0.

Expect: fzf lists every session on the socket — add a second session and the picker earns its keep. One pick moves a client from scratchy to projectx in the captured run below, pointer-only, nothing detached (§5).

Phase 4 · Teardown and verification

status)
    # The receipt: does the built thing match the spec?
    tmux -L "$SOCKET" list-windows -t "$SESSION" \
        -F '#{window_index}: #{window_name}  panes=#{window_panes} #{window_flags}'
    ;;
kill)
    tmux -L "$SOCKET" kill-server
    echo "socket '$SOCKET': server killed - sessions, windows, panes, all of it"
    ;;

Verification is a list-windows format string read against the spec table you drew first — the same predict-then-check the object-model labs ran. Teardown is one kill-server, surgical because the server is named: it ends every session on that socket and nothing else (§4). The receipt after is §3's: no server running on /private/tmp/tmux-501/cap, exit 1 — the correct description of an empty world, not a failure.

The whole script, assembled

#!/bin/sh
# projectx — workspace generator (tmux user guide, section 15 capstone)
# One named server per context; one session per project; three windows built
# to spec, with a guard so running it twice never builds twice.
# Subcommands: create | status | attach | kill
# ~/bin/projectx · requires: tmux; fzf for the attach picker

SOCKET="${PROJECTX_SOCKET:-work}"             # the named server (section 4)
SESSION="${PROJECTX_SESSION:-projectx}"       # the session this script builds
ROOT="${PROJECTX_ROOT:-$HOME/src/projectx}"   # where the project lives

cmd=${1:-create}

case "$cmd" in
create)
    # Guard: has-session is the cheap existence test (section 5).
    if tmux -L "$SOCKET" has-session -t "$SESSION" 2>/dev/null; then
        echo "$SESSION already exists on socket '$SOCKET' - nothing created."
        echo "re-enter it with: $0 attach"
        exit 0
    fi

    # The directories the windows will start in.
    mkdir -p "$ROOT/src" "$ROOT/docs"

    # Window 1 "edit": one pane, in the source root.
    tmux -L "$SOCKET" new-session -d -s "$SESSION" -n edit -c "$ROOT/src"
    # Window 2 "build": two panes side by side (section 7's split).
    tmux -L "$SOCKET" new-window -t "$SESSION" -n build -c "$ROOT"
    tmux -L "$SOCKET" split-window -h -t "$SESSION:build" -c "$ROOT"
    # Window 3 "docs": one pane, in the docs tree.
    tmux -L "$SOCKET" new-window -t "$SESSION" -n docs -c "$ROOT/docs"
    # Land on the editor.
    tmux -L "$SOCKET" select-window -t "$SESSION:edit"
    echo "created $SESSION on socket '$SOCKET' - enter it with: $0 attach"
    ;;
status)
    # The receipt: does the built thing match the spec?
    tmux -L "$SOCKET" list-windows -t "$SESSION" \
        -F '#{window_index}: #{window_name}  panes=#{window_panes} #{window_flags}'
    ;;
attach)
    # Section 12's tss pattern, one socket over: pick, then switch or attach.
    pick=$(tmux -L "$SOCKET" list-sessions -F '#{session_name}' 2>/dev/null | fzf) || exit 0
    [ -n "$pick" ] || exit 0
    if tmux -L "$SOCKET" switch-client -t "$pick" 2>/dev/null; then
        exit 0
    fi
    exec tmux -L "$SOCKET" attach -t "$pick"
    ;;
kill)
    tmux -L "$SOCKET" kill-server
    echo "socket '$SOCKET': server killed - sessions, windows, panes, all of it"
    ;;
*)
    echo "usage: $0 {create|status|attach|kill}" >&2
    exit 1
    ;;
esac

The run, captured

Before any of this was printed, the assembled script was executed end to end on a throwaway — create, verify, enter, leave, re-run against the guard, teardown — on the cap socket against a scratch root, pointed there purely by the environment overrides. The ### lines are capture-rig labels between real outputs (the pty client and the pane-driving method are §8 and §12's); everything else is verbatim.

$ PROJECTX_SOCKET=cap PROJECTX_ROOT=/tmp/pxroot /tmp/pxcap/projectx create
created projectx on socket 'cap' - enter it with: /tmp/pxcap/projectx attach
$ PROJECTX_SOCKET=cap /tmp/pxcap/projectx status
0: edit  panes=1 *
1: build  panes=2
2: docs  panes=1 -
$ tmux -L cap ls
projectx: 3 windows (created Sun Aug 23 22:58:08 2026)

### pty client attached to projectx; list-clients:
/dev/ttys025: projectx [80x24 xterm-256color] (attached,focused,UTF-8)
### chord sent: Prefix d
$ tmux -L cap list-clients
(no rows — exit 0; the client left, the session stayed)

$ tmux -L cap new-session -d -s scratchy        # a second session, so the picker picks
### client attached to scratchy; picker run in a pane of projectx:
$ tmux -L cap send-keys -t projectx:pick 'PROJECTX_SOCKET=cap /tmp/pxcap/projectx attach' Enter
### capture-pane -p -t projectx:pick (fzf is up):
▌ scratchy
▌ projectx
  2/2 ──────────────────────────────…──────
>
### typed proj, pressed Enter; the server says:
$ tmux -L cap list-clients -F '#{client_tty} -> #{client_session}'
/dev/ttys026 -> projectx

### the pick window cleaned up (kill-window), then the guard, on purpose:
$ PROJECTX_SOCKET=cap /tmp/pxcap/projectx create
projectx already exists on socket 'cap' - nothing created.
re-enter it with: /tmp/pxcap/projectx attach
$ tmux -L cap ls
projectx: 3 windows (created Sun Aug 23 22:58:08 2026)
scratchy: 1 windows (created Sun Aug 23 22:58:28 2026)

$ PROJECTX_SOCKET=cap /tmp/pxcap/projectx kill
socket 'cap': server killed - sessions, windows, panes, all of it
$ tmux -L cap ls; echo "rc=$?"
no server running on /private/tmp/tmux-501/cap
rc=1

Captured 2026-08-23 on a throwaway -L cap server, scratch root /tmp/pxroot; fzf 0.74.2; the author's everyday ~/.tmux.conf was loaded (stock indexes and flags throughout). The $0 in the guard's message is whatever path invoked the script — ~/bin/projectx for you. The fzf rule line's dash run is elided for width; both sessions listed, two rows, exactly what list-sessions -F '#{session_name}' fed it. The socket file lingers in /tmp/tmux-501/ after the kill — §4's macOS drift, not a live server.

Stretch · Snapshot before teardown

Docs-verified, not run here: the resurrect leg. §13 installed nothing, and this capstone keeps that discipline. If you did adopt tmux-resurrect there, the stretch is one chord before projectx kill: Prefix Ctrl+s writes the snapshot — sessions, windows, panes, layout, working directories — and Prefix Ctrl+r restores it after the reboot or the teardown. Remember the tradeoff before you rely on it: it restores structure, not scrollback and not running programs (§13's honest column). For everything else this lab needs, the script you just wrote is the restore — run projectx create again and the workspace rebuilds in four commands' worth of time.

Checkpoint

  • status matched your spec table row for row — panes counted, * on edit, - on docs — and you can say why those two flags and not others (§6's decoder)
  • The second create refused, exited 0, and changed nothing — and you can name the command whose exit status made that possible
  • The picker moved a client between sessions without detaching it, and from your plain shell it attaches — you know which code path each case takes
  • Teardown ended exactly one server, and the receipt read no server running with exit 1 — and you checked pgrep anyway

Teardown: projectx kill already did it — that was the point of phase 4. Adopting for real: copy the script to ~/bin/projectx, point ROOT at your project, and your first projectx create on the work socket is your last manual workspace setup.

Sources: tmux(1) — has-session, new-session/new-window/split-window (-c), select-window, list-windows, list-sessions, switch-client, attach-session, kill-server · design and guard pattern cross-referenced to this guide's §4, §5, §6, §7, §12, §13 · transcripts: the full capstone run executed 2026-08-23 on a throwaway -L cap server (create, status, pane paths, pty attach/detach chord, fzf picker driven in a pane with a live client, guard re-run, kill-server receipt), author's everyday servers untouched; scratch removed after capture

16 · Troubleshooting

Ten symptoms cover almost everything tmux will ever do to surprise you, and most of them are not bugs — they are the object model from §1 behaving exactly as built, in a place you were not looking. The table maps each symptom to its cause and its fix, with the section that taught the mechanism. Three causes can be reproduced on demand, and carry real transcripts below the table; the ones that cannot — a reboot, a font — state their mechanism plainly instead. This guide does not print imagined output, and the symptom section is where that rule matters most.

SymptomCauseFix
no server running on /tmp/tmux-501/… No server is: never started, exited when its last session ended (exit-empty on), or killed. It is the correct answer, not an error (§4's three-state table). Usually nothing — if you just killed it, that is success. Otherwise tmux new -s name starts one on demand. Check with tmux ls; echo $?.
Colors broken inside tmux The $TERM contract chain: pane programs see tmux-256color, and true colour needs the RGB feature resolved on the client — via COLORTERM or answered capability queries (§2, §14). Probe each link: show -s default-terminal, then list-clients -F '#{client_termfeatures}' — if RGB is missing, export COLORTERM=truecolor in the client's environment; last resort set -as terminal-overrides ',xterm*:RGB'. Transcript below.
ESC feels laggy; vim mode-switch is slow escape-time — the milliseconds tmux waits to tell a lone Esc from an arrow key's prefix — delays every standalone Esc by up to its full value (§11). set -s escape-time 10 — already the 3.7b stock default; the line exists for machines configured under older defaults. If the lag is exactly half a second, see the honesty note below the transcript.
Clipboard does not sync over SSH copy-command runs where tmux runs — the remote host, where your clipboard is not. The buffer stack never leaves the server (§9). OSC 52: the clipboard escape sequence is ordinary output, so it crosses SSH — needs set-clipboard allowed and a terminal that honors it. Test your pair; when it must be deterministic, set-buffer -w -t <tty> is the hammer.
Pane says Pane is dead; window lists as zsh[dead] remain-on-exit (a window option) kept the exited pane alive as a corpse — a pane split into the same window after the option was set inherits it (§6). respawn-pane -k -t target restarts it; probe with #{pane_dead}. Transcript below.
Server gone after a reboot The server is a plain process, not a daemon — nothing registers it with launchd/systemd, nothing restarts it, and the reboot cleared its socket (§4). None built in. Start a server the ordinary way; for unattended relaunch you build it deliberately — §13's continuum is the userland answer.
Box-drawing artifacts — borders and dividers garble Pane borders and dividers are line-drawing characters tmux emits and your terminal renders. Garbling means the render chain broke: a locale without UTF-8, or a font without the glyphs. Make LC_ALL/LC_CTYPE/LANG contain UTF-8; tmux -u forces UTF-8 output regardless of locale (man-sourced, both claims). Then it is your terminal's font.
Sessions missing after a macOS reboot Same root as the row above, crueler consequence: sessions lived in the server's RAM. Restarting tmux starts an empty server — the old state is unrecoverable by any flag (§4). Decide before the reboot: tmux-resurrect/continuum save and restore window structure — not scrollback, not running programs (§13) — or treat reboot as teardown and let projectx create (§15) rebuild.
Stuck in copy-mode — border stays yellow In the vi table Escape is clear-selection, not an exit — the self-loop in the copy-mode machine (§9). Emacs table: Escape does leave. q — leaves in both tables, copies nothing. Probe state with #{pane_in_mode}. Know your table: show -gw mode-keys.
Mouse scroll does nothing, or scrolls the wrong thing Stock ships mouse off: the wheel drives your terminal's own scrollback — the wrong buffer, since the real scrollback lives in the server (§9). set -g mouse on routes wheel-up into copy-mode -u. For terminal-native text selection instead, hold Shift/Fn (§7, §10).

Worked: the dead pane, probed and revived

Reproduced on demand: set remain-on-exit on for a window, then let its pane's process exit. The probe and the recovery, from the research run — §6 showed the corpse; here is the full arc including revival:

$ tmux -L dp display-message -p -t dp:0.0 'dead=#{pane_dead} dead_status=#{pane_dead_status} cmd=#{pane_current_command} flags=#{pane_flags}'
dead=1 dead_status=0 cmd=zsh flags=*
$ tmux -L dp capture-pane -p -t dp:0.0 | grep -n . | tail -2
1:macbook% exit
24:Pane is dead (status 0, Sat Aug 22 23:54:59 2026)
$ tmux -L dp respawn-pane -k -t dp:0.0
$ tmux -L dp display-message -p -t dp:0.0 'dead=#{pane_dead} cmd=#{pane_current_command}'
dead=0 cmd=zsh

Three receipts in five lines: the probe's dead=1, the pane's own last line naming itself dead with the exit status and a timestamp, and respawn-pane -k's dead=0 — a fresh process in the same pane, same id, same window. The scope drift worth remembering: remain-on-exit is a window option, so a pane split into the window afterward inherits it; in the same run, a second pane exited and stayed dead alongside the first.

Worked: escape-time, measured

The symptom everyone blames on "the network" or "vim", reproduced and measured on a real client — each row a fresh client, the option set before attach:

### fresh server, escape-time 500 set BEFORE attach: lag = 0.49990200996398926
### same server, escape-time 10 set BEFORE fresh attach: lag = 0.5000848770141602
### escape-time=1200: lag = 1.200463056564331

The third row is the honest demonstration: escape-time 1200 delayed a standalone Esc by 1.200 s — every vim mode-switch, every time. The fix is one server option, set -s escape-time 10. The first two rows are the honesty note: in that capture rig both values floored at ≈0.500 s, because when a terminal never answers tmux's startup capability queries, tmux raises an internal 500 ms escape-delay floor (§11, source-verified). Your real terminal answers, and gets the configured delay — but if your Esc ever lags by exactly half a second, the suspect is that capability handshake, not your config.

Worked: the color chain, link by link

"Colors are broken" is three questions wearing one symptom, and each has a probe. First the inner contract — what pane programs see:

$ tmux -L tc -f /dev/null new -d -s tc
$ tmux -L tc show -gv default-terminal
tmux-256color

Then the outer one — whether the client resolved true colour. Two identical clients, differing only in environment, receipts first (from §14's controlled run):

### receipts: each attach client's pid, its tty, and its COLORTERM (ps):
pid 77319 on ttys013: COLORTERM=truecolor
pid 77320 on ttys014: (no COLORTERM)
$ tmux -L t8fix list-clients -F '#{client_name} features=[#{client_termfeatures}] COLORTERM=#{client_colorterm}'
/dev/ttys013 features=[bpaste,ccolour,clipboard,cstyle,focus,RGB,title] COLORTERM=
/dev/ttys014 features=[bpaste,ccolour,clipboard,cstyle,focus,title] COLORTERM=

The row with COLORTERM=truecolor in its environment resolved RGB; the identical row without it did not. So the diagnostic order for washed-out colors: default-terminal as above, then client_termfeatures — and if RGB is absent, fix the client's environment (COLORTERM=truecolor) before touching terminal-overrides. Two drifts to carry with you: #{client_colorterm} is not a format variable on 3.7b (both rows print empty — §12's silent-unknown lesson), and if your terminal shows RGB with no COLORTERM, it answered tmux's queries — the other legitimate road (§14).

Sources: tmux(1) — escape-time, default-terminal, set-clipboard, remain-on-exit, respawn-pane, terminal-overrides, pane_dead; the -u flag (UTF-8 forcing) · tmux source, tag 3.7b — tty-keys.c escape-delay floor, via §11 · transcripts: research captures 2026-08-22 (dead pane + recovery, escape-time measurements, stock option dumps) and §14's controlled RGB pair (2026-08-23), all on throwaway servers; the reboot, font, and SSH-leg rows are mechanism statements, deliberately transcript-free — they were not reproduced here and will not be faked

17 · Cheat sheet

Dense on purpose. Every chord below is stock 3.7b, verified against list-keys on this machine, in §5's notation — Prefix means press Ctrl+b, release, then the key. Where the opinionated build of §10 displaces a chord, the card says so. The one lookup worth more than this page is built in: Prefix ? lists the live table, Prefix / finds one key across all of them (§11).

The universal starting patterns — most days need nothing else.
tmux new -A -s work          # the one command: create the session, or attach if it exists
tmux new -s work             # create named and attach; -d creates it detached instead
tmux attach -t work          # come back later; add -d to steal (detach other clients)
tmux ls                      # what is running; exit 1 = no server (§4's table)
tmux kill-session -t work    # end one session; tmux kill-server ends the whole server
tmux -L try -f /dev/null new -s t   # throwaway server: stock config, surgically killable

-A makes new behave like attach when the name already exists — the man page's own words — so one command covers leave-and-return forever. It is old, in the good way: shipped in 1.8, and 3.1 taught the no--s form to attach to the best existing session (both per tmux's CHANGES) — safe on any tmux this guide can imagine you running.

Sessions · §5

Prefix ddetach this client — everything keeps running
Prefix ssession tree — type to filter, Enter to land
Prefix $rename session
Prefix ( / )previous / next session, no detach
Prefix Dpick which client to detach

Command form: switch-client -t name — needs an attached client (§5's sharp edge).

Windows · §6

Prefix cnew window, made current
Prefix n / pnext / previous window
Prefix 09jump to index
Prefix llast window — the two-window ping-pong
Prefix wwindow and session tree
Prefix ,rename window
Prefix &kill window (with confirm)
Prefix M-n / M-pnext / previous window with an alert

Flags on the window list: * current, - last-used, ! bell, # activity, ~ silence, Z zoomed, M marked — §6's decoder.

Panes · §7

Prefix %split left-and-right
Prefix "split top-and-bottom
Prefix onext pane
Prefix ;previously active pane — ping-pong
Prefix qflash each pane's index
Prefix Spacecycle preset layouts
Prefix Ctrl+move the split boundary (repeatable)
Prefix ztoggle zoom
Prefix { / }swap with neighbor
Prefix !promote pane to its own window
Prefix xkill pane (with confirm)

Targets: %N is identity, N is position — scripts want ids (§7).

Copy-mode & buffers · §9

Prefix [enter copy-mode (border turns yellow)
hjkl / arrowsmove (vi / emacs tables)
? / /search up / down; n repeats
Space / C-Spacebegin selection (vi / emacs)
Enter / M-wcopy and leave (vi / emacs)
qleave, copy nothing — the reliable exit
Prefix ]paste top buffer, bracketed (no Enter)
Prefix # / = / -list / pick / drop buffers

Plain y is unbound in vi copy-mode — §9's misconception card has the one-line fix.

Clients & meta · §8, §11

Prefix Ctrl+zsuspend client; fg in the shell resumes
Prefix rforce redraw
Prefix ?list the whole key table
Prefix /which tables bind this key

Sizing is a client vote: window-size latest by default on 3.7 — the most recent toucher wins, smallest is opt-in (§8).

Command line · §12, §15

ls · has-session -t nlist sessions · existence probe by exit status
list-windows/panes/clients -F '#{…}'the server as a database
display-message -p -t t '#{…}'format printf for any target
send-keys -t t 'cmd' Entertype into a pane from outside
capture-pane -p -t tread a pane to stdout, clean
set/show-options -s/-g/-woptions by scope, live
source-file ~/.tmux.confreload — the file is a script
bind/list-keys · set-hookrebind tables · events with receipts

Every listing format above is §12's language; the assembled daily-driver version is projectx (§15).

Chords displaced by §10's opinionated build: |/- replace %/" as mnemonic splits (- was drop-buffer), r becomes reload (redraw moves), and the prefix itself is Ctrl+a — double-tap sends it through. Everything else stands.

Sources: every chord read off tmux list-keys -T prefix / -T copy-mode-vi / -T copy-mode on 3.7b — the same dumps behind the per-section tables of §5§9 and §11 · tmux(1) — new-session -A; DEFAULT KEY BINDINGS · tmux CHANGES, tag 3.7b — -A in 1.8, no-s form in 3.1, accessed 2026-08-23

18 · Glossary

Every term of art this guide uses, alphabetical, each defined in one or two sentences and linked back to the section that taught it — with the receipts. If a word in an earlier section slowed you down, it is defined here.

activity / silence
Window monitoring: monitor-activity flags a window that produced output since you last looked (#), monitor-silence flags one that has been quiet for the interval (~). Both are window options, set per window. §6
automatic renaming
The default behavior where an unnamed window is labeled by whatever its pane is running — zsh, vim — tracking the program as it changes. Named windows keep their names. §6
base-index
Session option for the first window's index — 0 stock. Read live at the moment a window is allocated, so a global change affects the next window created in any session, never existing ones. §6, §10
bracketed paste
A paste wrapped in markers that make the receiving shell treat it as literal text — embedded newlines park on the input line instead of acting as Enter. Why Prefix ] ships as paste-buffer -p. §9
buffer
A paste buffer: one held copy of text, named bufferN or a name you chose with -b. Created by any copy, by set-buffer, or by capture-pane -b. §9
buffer stack
The server-wide collection of paste buffers, newest on top, capped by buffer-limit (stock 50). Shared by every session on the server — unlike scrollback, which is per pane. the copy map (§9)
client
A process — the tmux attach you ran — plus one socket connection and one terminal it draws into. Not the terminal itself; the terminal is the client's furniture. Detaching removes a client and nothing else. §8
copy-mode
The mode that borrows the pane's keyboard back from your program: the same physical keys mean move, search, select, copy. Entered with Prefix [ or a wheel-up under mouse mode; the border turns yellow while you are in it. the copy-mode machine (§9)
dead pane
A pane whose process exited while remain-on-exit (a window option) kept it on screen — last output visible, last line reading Pane is dead (status N, …). Revived by respawn-pane -k. §16
escape-time
Server option: the milliseconds tmux waits after an Esc byte to decide whether it is a lone escape or the start of an arrow-key sequence. Every standalone Esc is delayed by up to the full value; stock 3.7b ships 10. §11
exit-empty
Server option, stock on: the server exits when its last session ends. Turned off, a server can sit alive with zero sessions — and ls prints nothing at exit 0. §4
format
tmux's template language: variables spell #{name}, evaluated by the server against a target object, with conditionals #{?cond,yes,no} and operators. The language of every -F listing and the status line. §12
global options
The fallback layer of session and window options. An object with no value of its own reads the global at the moment the option matters — live, not copied at creation; only materialized effects (allocated indexes, drawn borders) stick. §10
hook
A named event the server fires — window created, client detached — mapped by set-hook to a shell command run through run-shell. Fire detached, with no client attached; 57 names in the 3.7b catalog. the hook map (§12)
index
A position. Window indexes are per session; pane indexes are per window; both start at 0 and renumber when neighbors close. For talking about positions — not for scripts, which want ids. §7
key table
A named set of key bindings; which table is live is just state. The three you know: root (no prefix — mostly mouse), prefix (every chord), and the copy-mode tables. §11
layout
A preset dealing of pane positions: even-horizontal, even-vertical, main-horizontal, main-vertical, tiled, plus mirrored variants. Cycled by Prefix Space; ids and indexes keep their numbers while geometry changes. the pane map (§7)
One window object appearing in a second session's list — both sessions view the same panes and processes. link-window -s src -t session:index; the target is a slot by index, never a name to create. §6
marked pane
The pane flagged by select-pane -m (window flag M) — the remembered target for swap-pane and joins. Cleared with Prefix M. §7
mode-keys
The window option choosing copy-mode's table: vi or emacs. Its default follows the server starter's EDITOR/VISUAL — pin it to make it predictable. §9
mouse mode
set -g mouse on: the terminal's mouse reaches into tmux — click selects panes, drag resizes, wheel scrolls into copy-mode, drag copies. Stock is off; the keyboard chord for everything still exists. §7, §10
named server
A server reached by a socket of its own — -L name — in total ignorance of the default server and every other name. The unit of throwaways, config isolation, and hard project separation. §4
OSC 52
An escape sequence instructing whatever terminal displays it to put bytes on the system clipboard — ESC ] 52 ; ; <base64> BEL on 3.7b. Ordinary output, so it crosses SSH; emitted per the set-clipboard option and client features. §9
pane
One pseudo-terminal running one process — the only place in the object tree where work actually runs. Dies with its process unless remain-on-exit intervenes; the last pane out takes the window. §7
pane id
A pane's identity: %N, assigned server-wide in creation order, never reused, following the pane through breaks and joins. Inside a pane, $TMUX_PANE expands to it. What scripts target. §7
pipe-pane
A tee from a pane's raw output stream to a shell command, running for the pane's life or until a bare pipe-pane stops it. A faithful recording, not clean text. §12
popup
A rectangular overlay a client draws over the session, running a command — tmux's native floating window and the natural picker launcher. Not a pane: invisible to every list- command, dies with its client. §12
prefix
The chord-starting key: Ctrl+b stock, pressed and released before the command key — a shift key for commands, never held. Remappable to Ctrl+a with the double-tap tradeoff. §5, §10
repeat-time
Session option (stock 500 ms): the window in which a repeatable chord's key can be tapped again without re-pressing the prefix. Opt bindings in with bind -r. §11
run-shell
The command that runs a shell command from inside tmux — hooks run through it, and it works with zero clients attached. §12
scrollback
A pane's own history, kept by the server in RAM and capped per pane by history-limit (stock 2000 lines). Private to the pane; dies with it. Read through copy-mode or capture-pane. the copy map (§9)
server
One long-lived process that owns everything: sessions, windows, panes, the programs inside them, the scrollback, the buffers. Starts itself on first session creation, outlives every client, exits when its last session ends. §4
session
A workspace: the unit that survives detach, holding windows. Cheap, named, listed, switchable in one chord without detaching. Ended deliberately by kill-session — never by detach. §5
SIGHUP
The hangup signal: delivered by process group when a pseudo-terminal hangs up — a closed window, a dropped SSH, a slept laptop. It kills the shell that owns the connection; what happens to the shell's children is §2's whole story. tmux changes that story by re-parenting every pane's shell to the server. §2
socket
The Unix socket a server owns and clients connect through — one file per server under /tmp/tmux-$(id -u)/, mode 700. A dead server's file lingers on macOS; ask the server, not the directory. §4
status line
The one-row bar tmux draws: format strings (status-left, status-right, the window-list templates) rendered on the status-interval timer. Styled with the styles family. §14
style
A paint value — fg=/bg= plus attributes — taking a named colour, a 256 index, or a hex triple. Style options can be full formats, so conditionals paint. §14
suspend (client)
Prefix Ctrl+z: stop the client process and drop to the shell that launched tmux — connection kept, session still attached server-side. fg resumes. §8
switch-client
Move a client's pointer to another session, instantly, no detach — the backbone of scripted session-jumping. Needs a current client; refuses with no current client otherwise. §5
synchronize-panes
Window option sending every keystroke to every pane in the window. The border turns red while on and the window lists as sync; turn it off the moment the burst ends. §7
$TERM / terminfo
The capability contract: programs ask $TERM — not your terminal — what colors, cursor moves, and clipboard routes exist, via the terminfo database. tmux gives each pane its own tmux-256color identity while the outside terminal reports its own. §2
$TMUX / $TMUX_PANE
Environment variables tmux sets inside panes: $TMUX identifies the server (and is why nested attaches refuse), $TMUX_PANE expands to the pane's own id. How a script running inside a pane finds itself and its server — bare tmux just works. §12
window
The tab-like unit: one full-screen view inside a session, one visible at a time, holding panes. Windows are views; panes are where processes live. §6
window flags
The status line's per-window annotations: * current, - last-used, ! bell, # activity, ~ silence, Z zoomed, M marked pane. They stack in one string per window. §6
window-size
Session option deciding a shared window's size: latest (3.7 default — the most recent client to attach or resize wins, however small) or opt-in smallest. The folklore had it backwards. §8
zoom
Prefix z: the current pane fills its window; the others keep running behind it. A toggle and a lens, not a layout; the window wears Z from every session's view. §7

Sources: definitions consolidated from the sections linked above, each of which carries its own evidence and sources; terminology cross-checked against tmux(1) — TERMINALS, BUFFERS, HOOKS, FORMATS, STYLES, and the command pages for every named command.

19 · Index

Terms, commands, options, format variables, and chords, alphabetical, each linked to the section that taught it. Commands and options are set in code; chords are what your fingers know. Definitions live in §18; this page is for jumping.

Every entry links to the section whose transcripts introduced it; cross-check definitions in §18, dense chords in §17, symptoms in §16. Verified anchors — every link on this page resolves to a section or figure id in this file.

Sources: entry names and claims inherit from the sections they link to (§1§14), each of which ends with its own evidence line; terminology cross-checked against tmux(1) on 3.7b.

20 · Quiz

Eighteen cards, one per module of the course. Answer out loud before you reveal — the reveal button is a commitment device, not a shortcut — then mark yourself honestly: Got it or Review. The score line remembers your marks in this browser. The four checkpoint cards back in §5 are practice and never count here; the denominator is this page's eighteen.

Score: 0 learned · 0 review · 0/18 answered
Q1Your SSH connection drops while you are attached, a compile running in a pane. Name what ends, what survives, and where the survivors live.
The client ends — a terminal plus one socket connection, nothing more. The session, its windows, panes, the compile, and the scrollback keep running inside the server, and tmux attach from anywhere re-enters exactly where you left. Detach, deliberate or accidental, removes a client and nothing else. (§1)
Q2A colleague calls tmux panes "tabs." Correct them: which object is the tab-like unit, what do panes actually multiply, and where do processes live?
The window is the tab-like unit — one full-screen view at a time, holding panes. Panes are simultaneous splits of one window, several visible at once. And the pane is the unit that holds a process: close the pane and the process running in it ends with it. (§1)
Q3ls /tmp/tmux-501/ still shows your socket after kill-server. Is a server alive? Give the three possible answers tmux ls can give, with exit statuses.
The file's existence tells you nothing — on macOS a dead server leaves its socket behind, a drift captured in this guide. Ask the server: no server → no server running …, exit 1; server with sessions → one line per session, exit 0; server alive with zero sessions (exit-empty off) → silence, exit 0. (§4)
Q4Who starts the tmux server, what ends it, and why are your sessions gone after a reboot?
It starts itself, silently, on the first session creation, and by default exits when its last session ends (exit-empty on; turned off, it sits alive with zero sessions). It is a plain process, not a daemon — nothing registers it with launchd/systemd — and a reboot clears /tmp and the RAM your sessions lived in. (§4)
Q5Two other clients are mirroring your session. One command to take it for yourself — and what do their terminals show afterward? Then you kill-session the one you are attached to: what happens to your client?
tmux attach -d -t name-d detaches every other client as you arrive; their terminals fall back to their shells and the session itself never changed. Kill the session you are watching and detach-on-destroy on (the default) detaches your client too; set it off and the client switches to the most recently used survivor instead. (§5)
Q6Why does link-window -s alpha:1 -t beta:linked fail, what is the working form, and what happens when you dissolve the window's last link?
-t addresses a slot by index — session:index — never a name to create; tmux looked for a window called linked, did not find it, and said exactly that. Working form: -t beta:1, putting the same window object into beta at index 1 while it stays alpha:1. A window may not be linked to zero sessions — the last link out is the window's death (or -k, to say so explicitly). (§6)
Q7A window in your status line reads edit !-M. Decode all three flags, and state the invariant about * and -.
! — a bell rang in that window; - — the last-used window before the current one; M — the window contains the marked pane (select-pane -m, cleared with -M). Invariant: exactly one * (current) and one - per session, always — and flags stack into one string per window. (§6)
Q8A window holds panes %0, %1, %2 at indexes 0, 1, 2. You kill index 1. What does list-panes show afterward, and which of the two labels should scripts target?
%0 index=0 and %2 index=1%1 is gone forever (ids are never reused) and %2 slid down into the freed position, healing the split. Indexes are positions that renumber; ids are identities that survive every move. Scripts and bindings target %N#{pane_id} from outside, $TMUX_PANE from inside. (§7)
Q9State the exact meaning of -s and -t in break-pane, why -t alpha:1.2 and -t alpha:1 each fail, and what happens when you omit -s.
-s is the pane to break; -t is the destination window — the reverse of intuition. A destination must not carry a pane suffix (can't specify pane here), and an occupied index is refused (index in use: — add -a/-b to place after/before it). Omit -s and the server breaks its current pane — with no client attached that belongs to the most recently used session, which may not be yours. join-pane flips back to pane-level flags. (§7)
Q10Client A (100x30) is attached; client B (60x20) attaches; then A resizes itself to 40x15. Give the window size after each step on stock 3.7b — and how you would opt into the old folklore rule.
100x29 → 60x19 → 40x14. window-size latest, the default, gives the window to the most recent client to attach or resize, however small; the height is one row under the terminal's because the status line eats it. Smallest-wins is opt-in: set-option -t SESSION window-size smallest. (§8)
Q11In vi copy-mode you make a selection and press y. What happens? What is the real copy key, what command does it run, and where does the copy land?
Nothing — plain y is unbound in copy-mode-vi on 3.7b (only C-y, which scrolls; it is a keymap styled after vim, not vim). Enter is the copy key, running copy-pipe-and-cancel: copy and leave in one keypress. The copy lands on the buffer stack — server-wide, newest first, shared by every session. Want the reflex back? bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel. (§9)
Q12In vi copy-mode, what do Escape and q each do — and in which copy-mode table is Escape an exit?
Escape runs clear-selection only — a self-loop: you stay in the mode, and #{pane_in_mode} still reads 1. q cancels and leaves with nothing copied, in both tables. The exception is the emacs table, where Escape does cancel. The habit to take: q to leave. (§9)
Q13On a running server you set -g base-index 1. A session created before the change gets a new window — index 0 or 1? What does that prove about how options inherit, and which part of the old session never changes?
Index 1. An option with no value of its own falls back to the global layer at the moment it is read — live, not copied at creation (the mid-flight prefix flip proved the same rule). What sticks is the materialized effect: the old session's already-allocated 0-based indexes stay 0-based; renumbering is what move-window -r and renumber-windows are for. (§10)
Q14To undo a config you just sourced, you run source-file /dev/null. What happens to prefix, and what are the two honest ways to actually revert a setting?
Nothing — the file is a script, and sourcing an empty one ran zero commands; the earlier ones stand. Revert honestly by setting the option back explicitly, or by starting a fresh server, which falls back to factory defaults. (§10)
Q15What does bind -n C-l … do that bind C-l … does not, and what is the rule for choosing root-table keys?
-n binds in the root table — it fires on the bare key with no prefix, in every pane on the server, and the program in the pane never sees the key again (Ctrl+l stops clearing your shell's screen, stops redrawing vim). Rule: root-table bindings belong on keys nothing you run cares about — or keys you have decided tmux owns. (§11)
Q16list-sessions -F '#{session_name} created=#{session_created_string}' prints alpha created=. Does the timestamp not exist, or is something else going on? Name the discipline.
Something else: unknown variables expand to nothing, silently — #{session_created_string} is not a variable on 3.7b, while #{session_created} is (convert externally with date -r). Before trusting silence, check the man page's FORMATS list (#{client_colorterm} is the same family) — and single-quote whole format strings so your shell passes them through untouched. (§12)
Q17tmux has no plugin system. Mechanically, what is a plugin, and why must TPM's run '~/.tmux/plugins/tpm/tpm' line come last in your config?
A plugin is a git repository whose *.tmux script your server executes — it runs bind-key and set-option exactly as your own config does; there is no plugin API. The run line is the loader's trigger: when it executes, TPM reads the @plugin options set above it and sources each plugin. Config after the line still runs — the file is a script — but too late to affect plugin setup, which is exactly why your overrides belong there. (§13)
Q18Two identical clients, both TERM=xterm-256color; one carries COLORTERM=truecolor in its environment, one does not. Which resolves the RGB feature, how do you probe it, and why did §14's drift card have to correct an earlier capture?
The one with COLORTERM=truecolor — on these clients the gate is the environment variable, not the TERM name (the other legitimate road is a terminal that answers tmux's capability queries). Probe with list-clients -F '#{client_termfeatures}'; note #{client_colorterm} is not a format variable, so the feature list is the truth. The earlier capture lacked per-client receipts; a controlled run with ps-proven environments disagreed, and the guide teaches the run you can reproduce. (§14)

Aim for 16/18. Anything you marked Review carries its section link in the answer — follow it, re-read, and run that section's lab again. The questions are written against 3.7b behavior as captured in this guide, corrections and drifts included; if your machine answers differently, the section's transcript is the tiebreaker.

Sources: every question cross-links the section that taught it, and each section's own Sources line carries the transcripts and man-page citations behind the answer. Chord and option claims verified against tmux list-keys and show-options on 3.7b.

21 · Sources

Checked with tmux 3.7b · 2026-08-22 · latest stable at check: 3.7c · external links require network; the guide itself works offline

The evidence hierarchy, top to bottom: transcripts captured live on this machine (tmux 3.7b, throwaway servers only), the tmux(1) manual for command and option syntax, tmux's own source when the manual is ambiguous, and project documentation for everything in §13 — which was read, never run. Every section ends with its own Sources line naming exactly what backs it; this page is the consolidated index.

  • tmux(1) — OpenBSD manual — best used for command syntax and the OPTIONS, FORMATS, STYLES, and DEFAULT KEY BINDINGS sections; this guide's most-cited source.
  • tmux GitHub repository — best used for reading the source when the manual is ambiguous; the OSC 52 emission gates and the 500 ms escape-delay floor were verified in it (tag 3.7b).
  • tmux GitHub wiki — best used for getting-started material, the FAQ, and install pointers beyond §3's thirty seconds.
  • tmux CHANGES (master) — best used for what changed between versions: the 3.8 theme system (§14), when -A shipped (1.8), when the no--s form landed (3.1).
  • tmux releases — best used for tarballs when you must build from source, and for seeing how far your distribution's package lags.
  • Homebrew — tmux formula — best used for the macOS install path and the currently packaged version.
  • tmux-plugins/tpm — best used for the plugin manager's install steps and its @plugin option contract (docs-verified in §13, not run here).
  • tmux-plugins/tmux-resurrect — best used for the save/restore chords and exactly what a snapshot contains: structure, not scrollback.
  • junegunn/fzf — best used for the picker behind §12's tss: exit codes, search syntax, shell integration.
  • signal(7) — Linux man-pages — best used for SIGHUP semantics, the signal §2's honest pitch is built on.
  • fzf: a user guide (this repo) — further reading: the picker side of the partnership, from fuzzy-match syntax to reload-driven launchers.
  • lazygit: a user guide (this repo) — further reading: a full-screen TUI that pairs naturally with one pane per concern.

Sources: this page is the consolidated index; each section's own Sources line (§1§20) names the specific man-page sections, source files, and transcript dates behind its claims.