My personal tmux config, on the XDG path (~/.config/tmux/). tmux 3.1+ picks tmux.conf up from here automatically — no ~/.tmux.conf and no symlink needed.
Plugins are managed by TPM, which bootstraps itself on first load. The config ships with a test suite covering the conf statically, structurally and at runtime.
- tmux ≥ 3.2 (floating popups)
- macOS — the clipboard bindings use the native
pbcopy. On Linux, swap it forxclip/xselin the copy-mode bindings and in@extrakto_clip_tool. - Homebrew dependencies:
brew install fzf # required: extrakto + sessionx brew install lazygit # popup on prefix+G brew install bat fd # nicer sessionx preview (optional) brew install zoxide # sessionx suggests recent dirs (optional)
# 1. Clone into ~/.config/tmux
git clone https://github.com/albertosca/tmux.git ~/.config/tmux
# 2. Start tmux — TPM installs itself and fetches the plugins
tmuxOn the first launch tmux.conf notices TPM is missing and clones plus installs everything. To force it by hand: prefix + I.
Note:
plugins/is gitignored — every machine installs its own through TPM.
~/.config/tmux/
├── tmux.conf # main config
├── CHEATSHEET.md # shortcuts, popups, day-to-day flows
├── scripts/
│ ├── session-theme.sh # per-session status bar colors (work/personal)
│ └── sync-public.sh # publishes this config to the public mirror
├── test/
│ ├── run.sh # test entry point
│ ├── shell.sh # static checks against the conf
│ ├── integration.sh # runtime tests on an isolated socket
│ ├── structure.sh # line order, duplicates, plugin health
│ ├── meta.sh # harness self-tests
│ └── lib.sh # assertion helpers
└── plugins/ # managed by TPM (not versioned)
└── tpm/
| Section | Highlights |
|---|---|
| KEY BINDINGS | prefix C-a, splits |/-, vim nav hjkl, resize HJKL, popups, r = reload |
| DESIGN CHANGES | pane colors, status bar with sessions + git branch + date/time, true color |
| PLUGINS | TPM auto-install, 9 plugins, the TPM run on the last line |
| Post-TPM | overrides that must beat tmux-sensible defaults (e.g. status-keys vi) |
Load order matters: tmux-sensible sets status-keys emacs unconditionally when it loads, so anything that has to win against it lives after the TPM run line.
| Plugin | What it does | Binding |
|---|---|---|
| tpm | plugin manager | prefix+I install, prefix+U update |
| tmux-sensible | sane defaults | — |
| tmux-resurrect | save/restore sessions | prefix+C-s / prefix+C-r |
| tmux-continuum | auto-save every 15min + restore on boot | — |
| tmux-yank | copy to system clipboard | y in copy-mode |
| tmux-open | open the selection | o / C-o in copy-mode |
| vim-tmux-navigator | seamless Vim ↔ tmux nav | C-h/j/k/l without prefix |
| extrakto | fuzzy-grab paths/urls/hashes | prefix+Tab |
| tmux-sessionx | fuzzy session/window switcher | prefix+O |
The status bar and the pane borders change color based on the session name:
| Name prefix | Color |
|---|---|
work |
Cyan |
personal |
Orange |
| anything else | Default (black) |
scripts/session-theme.sh is driven by the session-created, session-renamed and client-session-changed hooks.
A window's tab changes colour on its own while a Claude Code session in that window is waiting on you, and you can mark a tab by hand to come back to it later.
| Tab colour | Meaning |
|---|---|
| Normal | nothing pending |
| Violet | a Claude finished, or asked something, and is waiting for you |
| Green | you marked this tab with prefix + m |
A manual mark takes precedence over a pending one and never erases it: marking a violet tab turns it green, and unmarking brings the violet back if that Claude is still waiting. Pending clears only when you actually reply — switching to the tab and reading does not count. The selected tab is shown in a lighter shade of whichever colour applies, in bold.
Adjacent coloured tabs are joined powerline-style: the wedge between two of them carries the previous tab's colour, and a run of same-coloured tabs is divided by a thin soft separator instead of an invisible same-on-same one.
This half lives outside this repo, in
~/.claude/hooks/tmux-pending.sh. Without itprefix + mis a no-op — the binding is guarded bytest -f— and everything else works unchanged.
bash test/run.sh # all four suites, compact output
bash test/run.sh -v # case by case
bash test/run.sh shell # static greps + regression guards
bash test/run.sh structure # line order, duplicates, encoding, plugin health
bash test/run.sh integration # runtime on an isolated socket
bash test/run.sh meta # harness self-testsThe integration suite starts its own tmux server on a PID-namespaced socket (claude-test-$$), so it never touches running sessions. Run it before committing — it encodes every bug already fixed here as a permanent regression test.
Two bindings reach outside this repo and are guarded so a fresh clone still works:
default-commandpins the native arm64reattach-to-user-namespaceby absolute path (a PATH-resolved Intel build silently forces every pane under Rosetta). The pin sits behind anif-shellexistence check — unguarded, an absolute path that is missing kills every pane with exit 127.prefix + mcalls a personal Claude Code hook that only exists on my machine, behind atest -fguard, so it is a no-op anywhere else.
For C-h/j/k/l to navigate inside Vim too, install the matching Vim plugin — see the Vim ↔ tmux section in CHEATSHEET.md.
- albertosca/vim — my Vim config (pairs with this setup: tmux↔Vim navigation, plugins, theme)
- albertosca/vim-tutorial — the Vim tutorial I use as a reference