Multiplexer
tmux: one terminal, many panes, work that can survive the window.
tmux splits a single terminal window into panes and tabs, keeps work running after you close the window, and gives agents a visible workspace beside your editor, tests, logs, and Git. Learn three ideas and a dozen keystrokes and you can supervise more work with less window juggling.
Panes
Windows
Sessions
tmuxp load start project layouttmux attach return laterTools & scrollback
#Why bother
Without tmux, one terminal window is one shell. With it, you get an editor, a running server, logs, and Git side by side — and if you close the laptop or the connection drops, that whole layout is still there when you come back. That last part, persistent sessions, is the feature people fall in love with.
#The three ideas
Everything in tmux is one of three things, nested inside each other.
- Session — a whole workspace for a project. You can have several and jump between them.
- Window — like a browser tab inside a session. Each fills the screen; you switch between them.
- Pane — a split within a window. This is where the actual shells and terminal apps live.
#The prefix — the one habit
Almost every tmux command starts with the prefix: press Ctrla, let go, then tap the next key. It's how tmux knows you're talking to it and not to the program in the pane.
So "split the pane" is really: Ctrla then |. Throughout this page, that pattern means prefix, then the key.
The mouse is enabled: click a pane to focus it, drag a border to resize, scroll to look back. Keys are faster once they stick, but the mouse is a fine way to start.
#Best-practice shape in this setup
The modern, low-friction pattern is one tmux session per project, a few clearly named windows inside it, and panes only where seeing two things at once helps. This repo now uses that pattern directly:
| sesh | Jump to an existing session or create one from a project folder with Ctrla then T |
| pane border labels | Every pane border shows its pane number, title, and current folder so full-screen apps like Claude, Codex, or Neovim do not hide your location |
| tmux-resurrect + continuum | Save periodic snapshots of sessions, windows, panes, directories, and layout so a new tmux server can restore the last checkpoint |
| tmuxp | Check a reusable project layout into a repo as .tmuxp.yaml when a team or class should start from the same windows and commands |
A good default shape for a project session is four named windows: edit for the editor or coding agent, shell for general commands, run for the dev server or test watcher, and git for lazygit and review. Create a window with Ctrla then c and name it with Ctrla then ,; resurrect snapshots keep the layout across restarts. Skip the permanent file-tree pane: lt shows a repo's shape on demand, and the fzf pickers (CtrlT files, AltC folders) pop up, do their job, and get out of the way.
That keeps the daily workflow simple: use sesh to move around, let continuum snapshot your state, and reach for tmuxp only when a project has a layout worth sharing or recreating from scratch.
#The essential moves
This is the custom keymap on this setup. You don't need all of it on day one — the panes and sessions rows are what matter first.
Panes
| Ctrla then | | Split left/right (a vertical divider) |
| Ctrla then - | Split top/bottom |
| Alth / j / k / l | Move between panes — and Neovim splits — no prefix needed |
| Ctrla then HJKL | Resize the pane (hold to repeat) |
| Ctrla then P | Rename the active pane — useful labels are claude, server, logs, tests |
| Ctrla then z | Zoom — make one pane fill the screen, then back. Nothing is lost: press it again to restore the layout, and the status line shows a Z while a pane is zoomed. This is the key people hit by accident when a pane suddenly seems to take over the screen. |
| Ctrla then x | Close the pane — it asks first, because x sits next to z and a mistyped zoom should never kill a pane running an agent, a server, or an unsaved editor |
Try it now: split and move
Type tmux and press Enter. Split side by side with Ctrla then |, then top/bottom with Ctrla then -. Hop between the panes with Alth/j/k/l. Run ls in one and top in another so they feel distinct.
Try it now: zoom and resize
Land on the busiest pane and press Ctrla then z to blow it up full-screen, then again to bring the others back. Now nudge a border: Ctrla then H a few times to widen it.
Windows
| Ctrla then c | New window |
| Ctrla then 1…9 | Jump to window by number |
| Ctrla then < / > | Move the current window left/right in the list |
| Ctrla then , | Rename the window |
| Ctrla then & | Close the current window and all panes inside it (tmux asks first) |
Best names are short nouns: edit, agent, server, logs, db, git. Window names show in the status bar; pane names show in the pane border.
Naming is hybrid, on purpose. A window you haven't named renames itself after whatever is running in it (zsh, nvim, claude); the moment you name one with Ctrla , your name sticks — auto-renaming turns off for that window only, and programs are never allowed to rename windows out from under you (they can only set the pane title in the border, which is the right place for an agent's task chatter). Renames are also saved into the restore state immediately, so a name you set survives a reboot instead of depending on the last 15-minute autosave. One habit that makes names largely unnecessary: keep one session per project (the session switcher, Ctrla T, names sessions after their folder), and let windows within it be task lanes.
Try it now: windows as tabs
Make two more windows with Ctrla then c. Jump straight to the first with Ctrla then 1. Rename one with Ctrla then , so the status bar reads something useful.
Sessions — the persistent part
| Ctrla then T | Open the session switcher (jump to or create a project workspace) |
| Ctrlx inside the switcher | Kill the highlighted session without leaving the picker; the list refreshes. Folder rows aren't live sessions, so it does nothing on those |
| Ctrla then s | tmux's built-in session tree — arrow through every session and window, Enter switches, x kills the highlighted one (tmux asks first), q quits |
| Ctrla then d | Detach — leave everything running and return to the plain shell |
| Ctrla then $ | Rename the session |
tmux ls | From the shell, list the sessions that are actually alive |
tmux attach | From the shell, reattach to where you left off (tmux attach -t name for a specific one) |
tmux-fresh | Start tmux without restoring the previous resurrect snapshot |
tmux kill-session -t name | End a specific session and everything running inside it |
tmux kill-session -a | Kill every session except the one you're in — the tidy-up move after a week of experiments |
tmux kill-server | End every live tmux session at once. Restore is separate and comes only from the last saved snapshot |
Ctrla then d is safe: it keeps the session running. Ctrla then x, Ctrla then &, and tmux kill-session end running shells and programs.
Ctrla then T opens sesh, not raw tmux ls. It shows live tmux sessions and useful project folders from zoxide, so seeing ~/code/roost or ~/code/groundwork in that picker does not always mean a tmux session is alive. After tmux kill-server, those folder rows can still appear because they are places you could start a session, not sessions still running. To see only live tmux sessions, run tmux ls or sesh list --tmux. To see only directory suggestions, run sesh list --zoxide.
Try it now: the magic trick, detach and return
Start something long-running, like ping example.com. Detach with Ctrla then d — you're back at the plain shell. Close the terminal entirely. Open a new one and type tmux attach. Everything is exactly where you left it, still running. That's the whole reason tmux exists.
Extras
| Ctrla then g | Open lazygit in a popup, in the current folder |
| Ctrla then S | Toggle "type into every pane at once" (great for many servers) |
| Ctrla then [ | Scroll/copy mode — v selects, y copies, q exits |
| Ctrla then ShiftY | Copy the last completed command and its output. A terminal's own "select last output" can't see inside tmux panes, so tmux owns this move, and the selection uses semantic prompt marks (OSC 133) emitted by the shell — a still-running command or prompt-looking text in output can't confuse it; in a pane whose shell lacks the marks it says so and points at copy-mode instead of guessing. The copy always lands in a tmux paste buffer (Ctrla then ]) and also reaches the system clipboard when the terminal supports it. A context line with the platform and working directory is prepended so pasted output tells an AI agent where it ran; very large output is visibly truncated (5,000 lines / 512 KiB). Review the copied text for secrets or personal/production data before pasting it into an AI tool. |
| Ctrla then Ctrly | Copy the current pane's working directory to the clipboard. A helper reads the path from tmux and pipes it to the buffer — it never runs the path through a shell, so a directory name with odd characters can't misbehave. |
| CtrlL | Clear the shell screen |
| Ctrla then CtrlK | Clear the shell screen and tmux scrollback |
| Ctrla then r | Reload the tmux config after editing it |
Try it now: sessions and lazygit
Open the session switcher with Ctrla then T and create a workspace for a project folder. Inside it, in any folder that's a Git repo, press Ctrla then g to pop open lazygit, then q to close it.
Try it now: scroll back
Output flew past? Press Ctrla then [ to enter scroll mode and move with the arrow keys or k/j. Press v to start selecting, y to copy, and q to drop back to normal.
#Selecting and copying: one owner per place
Selection lives in layers, and knowing which one you're in is the whole trick. Outside tmux, Ghostty owns the scrollback and the selection. Inside a tmux pane, tmux owns the history, search, and selection — that's why a terminal's "select last output" can't reach into a pane, and why Ctrla then [ is the way in. Inside a pane running a mouse-aware app (Neovim, lazygit), that app owns the mouse. The mouse is a convenience; the keyboard copy is the skill that travels — over SSH, on any terminal, from a trackpad or a keyboard.
Inside tmux → tmux owns history, search, selection, and copy. Inside a mouse app in tmux → the app owns the mouse. Outside tmux → Ghostty owns it. Need a raw one-screen grab inside tmux → hold Shift while you drag, which hands the mouse to Ghostty for a visible-screen selection. That escape hatch can't scroll or search tmux's history, so it's the exception, not the main road.
The keyboard copy
Ctrl+a [ enter copy mode
/error search backward for a string (n / N repeat)
v start selecting (Ctrl+v = block)
y copy — to the tmux buffer and the system clipboard
q leave
With the mouse you can still place a selection by dragging — but it now stays put when you release instead of vanishing, so you can adjust it or keep paging, then press y. Two shortcuts skip copy mode entirely: Ctrla then ShiftY grabs the last command and its output, and Ctrla then Ctrly grabs the current directory.
Right-click, and the macOS copy feel
Right-click does the sensible thing per layer: in an ordinary shell pane it shows a one-line copy hint (tmux's split/kill menu now lives on Option+right-click); in Neovim or lazygit it's handed straight to the app. On macOS, Ghostty uses an explicit copy — selecting text no longer auto-copies; press CmdC and the selection clears, matching every other Mac app. On Linux, Ghostty keeps the usual copy-on-select and middle-click-paste convention.
Copies leave through tmux's own OSC 52 route (set-clipboard external), so the same key works locally and over SSH. Programs running inside tmux can't reach your clipboard on their own — the one exception, yazi's inline image previews, is granted passthrough only on its own pane, then it's revoked.
Forgot a key? Ctrla then ? lists every binding (press q to leave). Edited the config? Ctrla then r reloads it. A printable cheat sheet lives at tmuxcheatsheet.com.
Running chezmoi apply inside tmux is okay. First check whether the command is simply busy: tool installs such as uv, mise, or Homebrew can sit quietly during downloads. From another pane, ps aux | rg -i 'uv|mise|brew|aider' can confirm. If nothing is running and output still seems frozen, press CtrlQ; CtrlS can freeze terminal output through flow control. Stop a truly stuck command with CtrlC. If tmux itself feels confused after a tmux/plugin update, detach with Ctrla then d, open a fresh Ghostty tab, and reattach with tmux attach.
#Project layouts as code
Two layers, one rule: the repo owns the commands (its package scripts and CLI), and your own config owns the arrangement. Personal session recipes live in ~/.config/sesh/sesh.toml — a name, a folder, and a startup command per project, so the prefix T switcher opens a project already doing something useful. Groundwork seeds that file with examples to copy.
When a project always wants the same multi-window arrangement — editor, agent, server, tests, logs — store that as .tmuxp.yaml in the project. That is the shareable version for teams and students. Resurrect restores whatever you personally had open; tmuxp starts a known layout from a file.
session_name: my-app
start_directory: ~/code/my-app
windows:
- window_name: edit
layout: main-vertical
panes:
- nvim .
- claude
- window_name: run
layout: even-horizontal
panes:
- pnpm dev
- pnpm test -- --watch
- window_name: git
panes:
- lazygit
tmuxp load .tmuxp.yaml | Start or attach to the layout described by the file |
tmuxp freeze > .tmuxp.yaml | Capture the current session as a starter config, then edit the commands and names |
sesh connect ~/code/my-app | Create or jump to a normal project session when you do not need a declared layout |
~/.config/sesh/sesh.toml | Your personal recipes: each entry names a project, its folder, and the command its first window runs |
Commit .tmuxp.yaml only when it helps someone else start the project. Do not put secrets, personal paths outside the repo, or one-off local commands in it. For private variations, use a local ignored file such as .tmuxp.local.yaml.
#The plugins
Most of what makes this tmux pleasant isn't tmux itself — it's a handful of plugins, managed by TPM (the Tmux Plugin Manager). You rarely touch them after install, but knowing what each one does explains where the magic comes from.
Managing them — TPM
Plugins are declared in the config (the set -g @plugin '…' lines). Groundwork runs TPM's command-line installer during chezmoi apply, so a fresh machine should not need a manual plugin install. These keys are still useful for repair and maintenance:
| Ctrla then ShiftI | Install any newly-declared or missing plugins and load them |
| Ctrla then ShiftU | Update installed plugins to their latest version. If the TPM pane looks blank or frozen, wait a few seconds, then press Esc or Enter to force a redraw and dismiss the summary. |
| Ctrla then Altu | Clean — remove plugins you've deleted from the config |
For fully visible output, run the same update from a shell instead of TPM's tmux pane: ~/.config/tmux/plugins/tpm/bin/update_plugins all.
The quiet helpers
Three plugins you'll never think about — they just make the defaults sane and the clipboard work:
| tmux-sensible | A base layer of non-controversial defaults (faster key response, bigger scrollback, focus events). Loaded first; it never overrides a setting you've made yourself. |
| tmux-yank | Makes copy-mode and mouse selections land in the real macOS clipboard, so y copies straight to ⌘V-able text. It's why selecting in tmux "just works." |
| vim-tmux-navigator | The reason Alth/j/k/l crosses seamlessly between tmux panes and Neovim splits — covered below. |
The status line — useful, not noisy
Full-screen terminal work hides the macOS menu bar, so the tmux status line carries the few machine signals worth seeing while you supervise an editor, tests, logs, or an agent: current app, CPU, RAM, host, battery when the machine has one, and time. It refreshes every 10 seconds, so it stays glanceable without becoming a tiny dashboard that constantly wakes the machine. Window and session renames repaint immediately; the 10-second pace applies only to the system modules.
Literal text such as #{cpu_percentage}, #{ram_percentage}, or #{battery_percentage} means the plugin/theme layer has not loaded. Re-run chezmoi apply to let Groundwork install missing TPM plugins, then reload with Ctrla then r. If you are already inside an old tmux server, restart tmux from a fresh Ghostty tab so the newly-installed plugins load cleanly.
Restore from snapshots — resurrect & continuum
This is the pair worth understanding, because together they let a tmux workspace come back after a reboot or fresh server start. They split the job in two:
- tmux-resurrect is the save/restore engine. It writes a snapshot of every session, window, and pane — their exact layout, each pane's working directory, and the programs running in them — to a file on disk, and can rebuild it later. This setup also turns on pane-contents capture, so your scrollback comes back too.
- tmux-continuum automates resurrect: it saves a fresh snapshot every 15 minutes. On a normal desktop profile this config sets
@continuum-restore 'on', so a new tmux server restores the last saved layout. The CI and agent container profile renders itoff, because surprise restore is wrong for disposable automation. Either way, it restores the latest snapshot, not whatever happened to be on screen at the instant you killed tmux.
You can also drive resurrect by hand whenever you want a checkpoint or an immediate restore:
| Ctrla then Ctrls | Save the current environment now |
| Ctrla then Ctrlr | Restore the last saved environment |
Layouts, working directories, and a conservative allow-list of programs (editors, less, top…) are restored, but not every arbitrary process, by design. Auto-restore fires when a new tmux server starts, not when you reload the config, and only if there is a saved snapshot. This setup stores snapshots under ~/.local/share/tmux/resurrect/. If sessions do not reappear automatically, check the newest file there, then restore manually with Ctrla then Ctrlr.
tmux kill-server really does end every live tmux session. It does not save first. If you want the current layout to be restorable, save a checkpoint with Ctrla then Ctrls before killing the server. On a desktop profile, the next tmux start may restore the last saved snapshot because @continuum-restore is on; inside the CI and agent container profile it stays off and you restore manually. If there is no recent snapshot, you get a clean session. For a deliberate clean slate, detach or open a fresh Ghostty tab and run tmux-fresh.
Seamless Vim ⇄ tmux movement — vim-tmux-navigator
Normally moving between tmux panes needs the prefix, and moving between Neovim splits is a separate set of keys. This plugin erases the seam: the same Alth/j/k/l moves you in a direction regardless of whether the neighbor is a tmux pane or a Neovim window — no prefix, no thinking about which is which.
It's a two-sided plugin: there's the tmux half (here) and a matching Neovim half managed in this Groundwork repo. The trick under the hood: when the key would leave a Neovim split at its edge, Neovim hands control to tmux, which moves to the adjacent pane — and vice versa. Plain CtrlL still reaches the shell, so clear-screen stays where terminal users expect it.
Full-screen terminal programs that also use Alt-h/j/k/l can intercept the movement. Just use the prefix to move that once; normal navigation resumes when you're back in a shell or editor.
The look — Catppuccin
The rounded, pastel status bar is the Catppuccin theme (the mocha flavor — the same palette as this guide, Ghostty, and your editor). The status line is built from composable modules: session name on the left, app/host/clock on the right. It's purely cosmetic; nothing breaks if you change or remove it. To try another flavor, set @catppuccin_flavor in the config to latte (light), frappe, or macchiato, then reload.
#Going deeper
When the basics feel automatic, these run from a gentle intro to the full reference:
- Official tmux wiki: Getting Started — the canonical intro to sessions, windows, and panes.
- Ham Vocke: A quick and easy guide to tmux — a clean ~10-minute hands-on starter.
- DreamsOfCode: tmux walkthrough (video) — the popular modern tour of a stack much like this one.
- DreamsOfCode: the Zen tmux configuration — nine concrete config improvements, explained.
- Smart tmux sessions with sesh — get the most out of the prefix T session switcher.
- The Tao of tmux — a full, free online book for going deep.
- awesome-tmux — the hub for plugins, themes, and tools.