Tmux and Neovim Together, A Keyboard Only Workflow That Sticks
TL;DR — tmux gives you project-scoped sessions and persistence across SSH drops. Neovim gives you the editor. The two together, with seamless pane navigation via vim-tmux-navigator, become a workflow where you don’t touch the trackpad for hours.
I resisted tmux for years. I had iTerm tabs. I had Neovim splits. Why add another layer? The answer turned out to be persistence and project scoping. When my laptop reboots or my SSH session drops, my tmux sessions survive. Neovim alone can’t do that.
The other reason is that once you have tmux, you stop thinking about windows and tabs at the OS level. Each project gets a tmux session. Inside each session, windows for editor, server, tests, logs. Switching projects is tmux attach -t backend-api. You never alt-tab again.
This post covers the integration that makes both tools feel like one. I’m running tmux 3.4 and Neovim 0.9.5.
The Setup, Three Files
The whole integration is three files and one plugin. Here’s the layout.
~/.tmux.conf:
# tmux 3.4 config
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",xterm-256color:Tc" # true color
set -g escape-time 10 # fix nvim escape lag
set -g focus-events on # let nvim know about focus
# Prefix to C-a, easier to reach than C-b
unbind C-b
set -g prefix C-a
bind C-a send-prefix
# Pane splits keep the current directory
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
unbind '"'
unbind %
# Reload config quickly
bind r source-file ~/.tmux.conf \; display "reloaded"
# vim-style pane navigation, with smart awareness of nvim
is_vim="ps -o state= -o comm= -t '#{pane_tty}' \
| grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'"
bind-key -n 'C-h' if-shell "$is_vim" 'send-keys C-h' 'select-pane -L'
bind-key -n 'C-j' if-shell "$is_vim" 'send-keys C-j' 'select-pane -D'
bind-key -n 'C-k' if-shell "$is_vim" 'send-keys C-k' 'select-pane -U'
bind-key -n 'C-l' if-shell "$is_vim" 'send-keys C-l' 'select-pane -R'
# Mouse on for occasional resize, off by default
set -g mouse on
Three details that took me too long to learn.
escape-time 10 matters. The default is 500ms, which means Neovim’s <Esc> key feels laggy inside tmux. Lower it.
focus-events on lets Neovim notice when its pane loses focus. Without it, autoread doesn’t work reliably when you edit a file in another tool.
The is_vim check is the magic that makes C-h/j/k/l navigate tmux panes when you’re in the shell, and Neovim splits when you’re in the editor. Same keys, context-sensitive behavior.
The Neovim Side
The matching Neovim plugin:
-- lua/plugins/tmux.lua
return {
{ 'christoomey/vim-tmux-navigator',
cmd = {
'TmuxNavigateLeft', 'TmuxNavigateDown',
'TmuxNavigateUp', 'TmuxNavigateRight',
},
keys = {
{ '<C-h>', '<cmd>TmuxNavigateLeft<cr>' },
{ '<C-j>', '<cmd>TmuxNavigateDown<cr>' },
{ '<C-k>', '<cmd>TmuxNavigateUp<cr>' },
{ '<C-l>', '<cmd>TmuxNavigateRight<cr>' },
} },
}
That’s it on the editor side. The plugin checks whether the next pane in a direction is Neovim or tmux, and either moves the cursor inside Neovim or sends the keystroke to tmux to switch panes. Seamless.
After this is configured, C-h and friends always do the right thing. Whether you’re in the editor, a shell pane, or a server logs pane, the same four keys navigate.
Session Persistence That Actually Works
The other half of the tmux value proposition is session persistence. Two plugins make this work reliably:
# Add to ~/.tmux.conf
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 @resurrect-strategy-nvim 'session'
run '~/.tmux/plugins/tpm/tpm'
After installing TPM and running prefix + I to install the plugins, your tmux state survives reboots. tmux-continuum saves snapshots every 15 minutes. tmux-resurrect restores them on startup.
The resurrect-strategy-nvim 'session' line tells resurrect to also restore Neovim sessions. For that to work, you need Neovim configured to write a session file on exit. The simplest approach is mini.sessions or a hand-rolled autocmd:
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
if vim.fn.argc() == 0 and vim.fn.exists(':mksession') == 2 then
local session_dir = vim.fn.stdpath('data') .. '/sessions'
vim.fn.mkdir(session_dir, 'p')
local session_file = session_dir .. '/' .. vim.fn.getcwd():gsub('/', '%%') .. '.vim'
vim.cmd('mksession! ' .. vim.fn.fnameescape(session_file))
end
end,
})
This writes a session file per directory. Pair it with a startup hook that loads the matching session if one exists, and you have persistent workspaces.
The Project Pattern
Here’s how the day-to-day looks. I have a shell function called tat (tmux-attach-or-create):
# In ~/.zshrc or ~/.bashrc
tat() {
local name="${1:-$(basename "$PWD" | tr . -)}"
if tmux has-session -t "$name" 2>/dev/null; then
tmux attach -t "$name"
else
tmux new-session -s "$name" -c "$PWD"
fi
}
cd ~/projects/backend-api && tat either attaches to an existing session for that project or creates a new one. Each project ends up with a dedicated session, named after the directory.
Inside a session, I use windows for distinct concerns:
- Window 1: editor (nvim) and a shell pane for ad-hoc commands
- Window 2: server (whatever runs the app)
- Window 3: tests (so I can rerun without losing scrollback in the editor pane)
- Window 4: database client or logs
prefix + 1/2/3/4 jumps between windows. prefix + c creates a new one. After a week, the layout is reflexive.
This is the workflow I described in passing in Bootstrapping Neovim with lazy.nvim and Mason, the editor config matters but it’s only useful inside a tmux session that survives your laptop’s quirks.
Copy/Paste, the Annoying Part
The one piece that breaks for everyone the first time is the clipboard. tmux’s copy buffer is internal. macOS’s clipboard is external. Neovim’s clipboard is configurable.
The fix on macOS:
# ~/.tmux.conf
set -g default-command "reattach-to-user-namespace -l $SHELL"
bind -T copy-mode-vi v send-keys -X begin-selection
bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "reattach-to-user-namespace pbcopy"
And on Linux, swap reattach-to-user-namespace pbcopy for xclip -selection clipboard or wl-copy depending on your display server.
On the Neovim side, vim.opt.clipboard = 'unnamedplus' makes yanks go to the system clipboard. Set this and y in Neovim shares a buffer with Cmd+C in the browser. The seam disappears.
Common Pitfalls
A few traps.
- Forgetting
escape-time. Default is 500ms. Neovim feels broken inside tmux until you set it to 10. - True color in screen-256color. If colors look washed out, set
default-terminal "tmux-256color"and add theTcoverride. Without both, true-color schemes degrade silently. - Mismatched prefix. If your muscle memory is
C-b, keep it. The “switch toC-a” advice is opinionated, not gospel. But pick one and stick with it. - Running tmux inside tmux. Happens when you SSH into a remote machine that also auto-starts tmux. The nested session breaks pane navigation. Set up a
TERMcheck or justtmux detachfirst. - Trying to use the mouse. Mouse mode works but it fights you on selection (selecting text in mouse mode doesn’t copy to the system clipboard by default). Use copy mode instead.
Wrapping Up
tmux plus Neovim is the most stable keyboard-only workflow I’ve found. Sessions survive reboots. Panes navigate with the same four keys whether you’re in the editor or a shell. Projects get their own tmux session and you stop thinking about OS-level window management.
The setup is a one-time investment of maybe an hour. The payoff is years.
The tmux man page is the canonical reference, and the vim-tmux-navigator README covers edge cases (zoomed panes, popup windows) that aren’t obvious from the config above. Both worth bookmarking.