Third time’s the charm. tmux has been installed and waiting since the Ansible post, and after two detours through eza, fzf, and zsh plugins, it’s finally getting its configuration. No more excuses.
The goal today: a .tmux.conf that feels right from the first keystroke, plus a plugin manager to handle session persistence — because losing your tmux layout to a reboot is the kind of pain you only tolerate once.
Installing TPM#
Before any plugins can work, tmux needs TPM — the Tmux Plugin Manager. It’s a single git clone:
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpmTPM lives in ~/.tmux/plugins/tpm/ and bootstraps itself from a line at the bottom of your .tmux.conf. When you launch tmux and press prefix + I (capital I), TPM reads the plugin declarations from the config and installs them into ~/.tmux/plugins/. Simple, no magic.
For our chezmoi setup, we’ll manage this the same way we handled the zsh plugins — via chezmoi externals. Add this to .chezmoiexternal.toml:
[".tmux/plugins/tpm"]
type = "git-repo"
url = "https://github.com/tmux-plugins/tpm.git"
refreshPeriod = "168h"Now chezmoi init --apply clones TPM automatically. One less manual step on a fresh machine.
The Configuration#
Here’s the full .tmux.conf. I’ll walk through each section below:
set -g prefix C-a
unbind C-b
bind C-a send-prefix
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -g history-limit 5000
set -sg escape-time 0
set -g mouse on
setw -g mode-keys vi
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "xclip -sel clip"
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
unbind '"'
unbind %
bind r source-file ~/.tmux.conf \; display "Reloaded!"
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-restore 'on'
set -g @continuum-save-interval '15'
if "test ! -d ~/.tmux/plugins/tmux-resurrect" \
"run '~/.tmux/plugins/tpm/bin/install_plugins'"
run '~/.tmux/plugins/tpm/tpm'It’s not long, but every line is intentional.
Prefix Key: Ctrl+a#
set -g prefix C-a
unbind C-b
bind C-a send-prefixThe default tmux prefix is Ctrl+b, and it’s awkward. Ctrl+a is easier to reach — your pinky stays on Ctrl while your thumb hits A. This is one of those changes that every tmux guide recommends, and for good reason. The send-prefix binding means pressing Ctrl+a twice sends a literal Ctrl+a to the running application, so you don’t lose that key entirely.
Sensible Defaults#
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -g history-limit 5000
set -sg escape-time 0
set -g mouse onWindows and panes start at 1 instead of 0 — because reaching for prefix + 0 on the far side of the keyboard when you only have one window makes no sense. renumber-windows fills gaps when you close a window in the middle, so you don’t end up with windows numbered 1, 3, 5.
The escape-time 0 is important if you use vim inside tmux. Without it, tmux waits after you press Escape to see if it’s part of a key sequence, which adds a noticeable delay when switching vim modes. Setting it to zero makes Escape instant.
Mouse support is on because sometimes you just want to click on a pane or scroll with the wheel. No shame in that.
Vi-Style Copy Mode#
setw -g mode-keys vi
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "xclip -sel clip"tmux has a built-in copy mode (enter it with prefix + [), and by default it uses emacs-style keybindings. Setting mode-keys vi switches to vi-style navigation — hjkl to move, / to search. The custom bindings make v start a selection (like visual mode in vim) and y yank it to the system clipboard via xclip. This makes copying text out of tmux feel natural if your fingers already know vim.
Intuitive Splits#
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
unbind '"'
unbind %The default split bindings (" and %) are impossible to remember. | for a vertical split and - for a horizontal split are visually obvious — the character looks like the split it creates. The -c "#{pane_current_path}" part ensures new panes open in the same directory as the current one, instead of defaulting to $HOME. A small thing that saves a cd every single time.
Quick Reload#
bind r source-file ~/.tmux.conf \; display "Reloaded!"prefix + r reloads the config without restarting tmux. Essential when you’re iterating on the configuration — change a line, reload, see the result. The display command gives visual feedback so you know it actually worked.
Plugins: Resurrect and Continuum#
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-restore 'on'
set -g @continuum-save-interval '15'These two plugins solve tmux’s biggest weakness: sessions don’t survive a reboot. If you’ve carefully arranged windows and panes across multiple projects, a restart wipes all of it.
tmux-resurrect saves and restores tmux sessions — windows, panes, layouts, and even the working directory of each pane. You can manually save with prefix + Ctrl+s and restore with prefix + Ctrl+r.
tmux-continuum builds on resurrect by saving automatically. With save-interval set to 15, it saves your session every 15 minutes in the background. And continuum-restore 'on' means when tmux starts, it automatically restores the last saved session. You reboot, open tmux, and everything is back — windows, panes, directories, all of it.
Together, they make tmux sessions feel permanent.
The Bootstrap Line#
run '~/.tmux/plugins/tpm/tpm'This must be the last line in the file. It initializes TPM, which then loads all declared plugins. After saving the config and launching tmux, press prefix + I (capital I) to install the plugins for the first time. TPM will clone them into ~/.tmux/plugins/ and load them immediately.
That works — but it’s a manual step. On a fresh machine, you’d open tmux, remember you need to press prefix + I, wait for the install, and then your plugins are available. It’s not terrible, but it’s not hands-free either. And the whole point of this series is eliminating those “oh right, I need to do that” moments.
Automatic Plugin Installation#
TPM ships with a bin/install_plugins script that does exactly what prefix + I does, but from the command line. We can hook into that from the config itself. Since chezmoi externals already handle cloning TPM, we just need to detect whether the plugins have been installed yet:
if "test ! -d ~/.tmux/plugins/tmux-resurrect" \
"run '~/.tmux/plugins/tpm/bin/install_plugins'"This checks if the resurrect plugin directory exists. If it doesn’t — meaning this is a fresh machine or plugins haven’t been installed yet — it runs TPM’s install script automatically. On subsequent launches, the directory exists and the check is skipped entirely. No delay, no manual step.
Add this line right before the final run line. The updated end of the config looks like this:
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-restore 'on'
set -g @continuum-save-interval '15'
if "test ! -d ~/.tmux/plugins/tmux-resurrect" \
"run '~/.tmux/plugins/tpm/bin/install_plugins'"
run '~/.tmux/plugins/tpm/tpm'First tmux launch on a fresh machine: chezmoi has already placed TPM, tmux starts, the if check triggers install_plugins, resurrect and continuum are cloned, and TPM loads everything. Fully automatic.
What We’ve Achieved So Far#
After chezmoi init --apply, a fresh machine now has:
- zsh installed and set as default shell
- The correct version of age, installed from source
- SSH keys decrypted and in place
- Git identity configured for the dotfiles repo
- SSH key linked to git, remote switched to SSH
- Ansible installed and used to manage software packages
- tmux installed via Ansible
- eza installed from an external apt repository via Ansible
- Onchange script that watches the entire
ansible/directory for changes - Sudo password support for non-root environments
- fzf installed from a GitHub binary release via Ansible
- eza aliases configured in .zshrc — ls, la, lla, and lls
- Pure prompt, autosuggestions, and syntax highlighting — managed via chezmoi externals
- fzf previews with bat (files) and eza (directories)
- tmux configured with sane defaults, vi-style copy, and intuitive splits
- TPM managed via chezmoi externals, with auto-installing resurrect and continuum for persistent sessions
What’s Next#
Right now, chezmoi init --apply assumes the age key is present — which means encrypted files like SSH keys get decrypted and placed. That’s exactly what you want on a trusted machine. But what about machines where you don’t want to expose your age key at all? Without it, chezmoi fails on any encrypted file, and the git remote stays on HTTPS since there are no SSH keys to switch to.
Next up: making the dotfiles work gracefully without the age key. Using chezmoi’s templating, we can conditionally skip encrypted files — like .ssh/id_ed25519 — when the key isn’t present, so chezmoi applies everything it can and quietly skips what it can’t. A single dotfiles repo that adapts to both trusted and untrusted machines.
