Groundwork

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.

When you'll want this You're juggling six terminal tabs and keep losing track of which is running the server, tests, or agent.
Quick reference — prefix is Ctrl+a

Panes

prefix | split right
prefix - split down
Alth/j/k/l move left/down/up/right
prefix P rename pane
prefix z zoom active pane
prefix x close active pane

Windows

prefix c new window
prefix 19 jump by number
prefix , rename window
prefix </> move window left/right

Sessions

prefix T session switcher
prefix d detach, keep running
prefix $ rename session
tmuxp load start project layout
tmux attach return later

Tools & scrollback

prefix g lazygit popup
prefix [ scroll/copy mode
prefix Ctrls save layout now
status line app, CPU, RAM, host, battery, time
prefix r reload config
prefix ShiftI repair plugins
prefix ShiftU update plugins

#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.
SESSION: groundwork 0: edit * 1: serve 2: git ← windows (tabs) pane: nvim pane: shell pane: logs the active window, split into three panes
One session holds windows (like tabs). The open window is split into panes. Sessions keep running even after you detach.

#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.

Mouse works too

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:

seshJump to an existing session or create one from a project folder with Ctrla then T
pane border labelsEvery 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 + continuumSave periodic snapshots of sessions, windows, panes, directories, and layout so a new tmux server can restore the last checkpoint
tmuxpCheck 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 / lMove between panes — and Neovim splits — no prefix needed
Ctrla then HJKLResize the pane (hold to repeat)
Ctrla then PRename the active pane — useful labels are claude, server, logs, tests
Ctrla then zZoom — 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 xClose 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
1

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.

2

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 cNew window
Ctrla then 19Jump 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.

3

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 TOpen the session switcher (jump to or create a project workspace)
Ctrlx inside the switcherKill the highlighted session without leaving the picker; the list refreshes. Folder rows aren't live sessions, so it does nothing on those
Ctrla then stmux'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 dDetach — leave everything running and return to the plain shell
Ctrla then $Rename the session
tmux lsFrom the shell, list the sessions that are actually alive
tmux attachFrom the shell, reattach to where you left off (tmux attach -t name for a specific one)
tmux-freshStart tmux without restoring the previous resurrect snapshot
tmux kill-session -t nameEnd a specific session and everything running inside it
tmux kill-session -aKill every session except the one you're in — the tidy-up move after a week of experiments
tmux kill-serverEnd every live tmux session at once. Restore is separate and comes only from the last saved snapshot
Detach vs close

Ctrla then d is safe: it keeps the session running. Ctrla then x, Ctrla then &, and tmux kill-session end running shells and programs.

The session switcher is broader than tmux

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.

4

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 gOpen lazygit in a popup, in the current folder
Ctrla then SToggle "type into every pane at once" (great for many servers)
Ctrla then [Scroll/copy mode — v selects, y copies, q exits
Ctrla then ShiftYCopy 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 CtrlyCopy 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.
CtrlLClear the shell screen
Ctrla then CtrlKClear the shell screen and tmux scrollback
Ctrla then rReload the tmux config after editing it
5

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.

6

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.

Which layer am I in?

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

copy from a pane's history
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.

One clipboard path

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.

When you're stuck

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.

If tmux looks frozen

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.

.tmuxp.yaml
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.yamlStart or attach to the layout described by the file
tmuxp freeze > .tmuxp.yamlCapture the current session as a starter config, then edit the commands and names
sesh connect ~/code/my-appCreate or jump to a normal project session when you do not need a declared layout
~/.config/sesh/sesh.tomlYour personal recipes: each entry names a project, its folder, and the command its first window runs
What to commit

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 ShiftIInstall any newly-declared or missing plugins and load them
Ctrla then ShiftUUpdate 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 AltuClean — 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-sensibleA 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-yankMakes 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-navigatorThe 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.

If you see placeholders

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 it off, 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 CtrlsSave the current environment now
Ctrla then CtrlrRestore the last saved environment
What does and doesn't come back

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.

What kill-server does

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.

If a key gets "swallowed"

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: