What ships with OpenBSD by default
Run `tmux -V` on a fresh OpenBSD 7.6 install and you get `tmux 3.4`. No package manager involved. The binary is at /usr/bin/tmux and the man page at man 1 tmux covers every option - 4,800 lines as of 7.6. Unlike Linux distributions that lag tmux releases by months or split it across contrib repos, OpenBSD tracks tmux upstream directly because Nicola Soulie and Nicholas Marriott, two core tmux developers, contribute closely with the OpenBSD project.
The implication for ops work: you can write a .tmux.conf once and trust it will behave identically across every OpenBSD host you manage. There is no `tmux-next` vs `tmux-stable` split to account for. On our test server running OpenBSD 7.6 on an amd64 VPS, tmux starts in under 40ms and consumes roughly 2MB RSS per session with four panes open.
tmux -V
# tmux 3.4
which tmux
# /usr/bin/tmux
wc -l /usr/share/man/man1/tmux.1
# 4823 /usr/share/man/man1/tmux.1
Base ~/.tmux.conf configuration
The default tmux prefix is C-b, which conflicts with readline's backward-char. Changing it to C-a matches the screen convention that most long-time sysadmins have in muscle memory. The following config is what we run on production OpenBSD jump hosts - no plugins, no TPM, no external dependencies.
Key choices explained: `set -g history-limit 50000` gives you 50,000 lines of scrollback per pane, adequate for grepping through log tails without eating RAM on memory-constrained jails. `set -g mouse on` lets junior team members click to select panes without learning keybinds, while not breaking anything for keyboard-only operators. `set -sg escape-time 0` eliminates the 500ms delay that makes vim feel sluggish inside tmux - this is the single most impactful setting for interactive use.
`set -g default-terminal screen-256color` is the correct value on OpenBSD. Do not use tmux-256color here unless you have the ncurses terminfo entry installed, which is not guaranteed on remote hosts you SSH into from your tmux session. screen-256color is universally present.
# ~/.tmux.conf - OpenBSD production baseline
# Prefix
unbind C-b
set -g prefix C-a
bind C-a send-prefix
# Core behavior
set -sg escape-time 0
set -g history-limit 50000
set -g mouse on
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -g default-terminal screen-256color
set -ga terminal-overrides ",xterm-256color:Tc"
# Reload config
bind r source-file ~/.tmux.conf \; display "Config reloaded"
# Split panes, open in current directory
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
unbind '"'
unbind %
# Vim-style pane navigation
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# Resize panes
bind -r H resize-pane -L 5
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5
# Copy mode
setw -g mode-keys vi
bind Enter copy-mode
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi y send -X copy-selection-and-cancel
# Status bar
set -g status-interval 5
set -g status-position bottom
set -g status-left "[#S] "
set -g status-right "#H | %Y-%m-%d %H:%M"
set -g status-left-length 30
set -g status-right-length 50
# Window activity
setw -g monitor-activity on
set -g visual-activity off
Session management patterns for ops work
The most common tmux mistake we see on ops teams is using a single unnamed session with windows numbered sequentially. After a week, nobody remembers what window 7 was for. Named sessions mapped to projects or systems solve this immediately.
Create sessions with descriptive names at the start of each work context. If you are naming a new project and need a clean identifier that also works as a hostname or domain label, the same discipline applies - readable, lowercase, hyphen-separated. Tools like nicename.me enforce this kind of naming convention automatically for domain registration, which translates well to session naming hygiene.
For persistent ops work, we use a standard session layout: one session per managed system or service cluster, with windows named by function. The `new-session -d` flag creates the session detached, useful in provisioning scripts.
# Create named sessions
tmux new-session -d -s webserver -n logs
tmux new-session -d -s database -n psql
tmux new-session -d -s build -n compiler
# List sessions
tmux ls
# Attach to specific session
tmux attach -t webserver
# Switch between sessions without detaching
# C-a $ to rename current session
# C-a ( and C-a ) to move between sessions
# C-a s for interactive session/window tree
# Kill a specific session
tmux kill-session -t build
# Send a command to a pane in another session without attaching
tmux send-keys -t database:psql "SELECT pg_size_pretty(pg_database_size('prod'));" Enter
Scripted session layouts with tmux-start scripts
Rather than TPM or tmuxinator, we prefer plain shell scripts that call tmux directly. They are auditable, work under doas, and have no Ruby or Python dependency. The following script creates a three-pane ops layout: top-left for shell work, top-right for log tailing, bottom spanning full width for an htop instance. On OpenBSD, htop is available via pkg_add and integrates cleanly.
This script is idempotent - it checks whether the session already exists before creating it. That makes it safe to call from .profile or a cron job without spawning duplicate sessions. We store these scripts in ~/bin/ with execute permissions and name them after the session they create, so `~/bin/webserver` launches the webserver session.
For teams managing multiple OpenBSD hosts, automating these session setups across servers via SSH is straightforward. If you are evaluating AI-assisted DevOps automation for tasks like this - pushing config scripts to fleets of hosts and verifying session state - taskbotshub.ai covers that workflow with agent-based job definitions that integrate with OpenBSD's rcctl and doas.
#!/bin/sh
# ~/bin/webserver - launch standard webserver ops session
SESSION="webserver"
LOGDIR="/var/log/httpd"
# Bail if session already exists
tmux has-session -t "$SESSION" 2>/dev/null && {
echo "Session $SESSION already exists. Attaching."
exec tmux attach -t "$SESSION"
}
# Create session with first window
tmux new-session -d -s "$SESSION" -n shell
# Split top half: shell left, logs right
tmux split-window -h -t "$SESSION:shell"
tmux send-keys -t "$SESSION:shell.2" "tail -F ${LOGDIR}/access.log" Enter
# Split bottom: full-width htop
tmux split-window -v -t "$SESSION:shell.1"
tmux send-keys -t "$SESSION:shell.3" "htop" Enter
# Select the shell pane
tmux select-pane -t "$SESSION:shell.1"
# Create a second window for config editing
tmux new-window -t "$SESSION" -n config
tmux send-keys -t "$SESSION:config" "cd /etc/httpd && ls" Enter
# Go back to first window
tmux select-window -t "$SESSION:shell"
exec tmux attach -t "$SESSION"
SSH and nested tmux across hosts
On OpenBSD jump hosts, you will regularly SSH from inside a tmux session into another machine and want to start or attach a tmux session there. The nested prefix problem - where C-a reaches the outer tmux and never the inner - needs an explicit solution.
The cleanest approach is a second prefix binding only active inside the nested session. We bind `C-a a` to send C-a through to the inner session. This means one keypress of C-a reaches the outer tmux, and C-a a reaches the inner one. It is low cognitive load once memorized.
For persistent connections that survive network drops, tmux on the remote end handles reconnection automatically - your session is still running. The only requirement is that tmux was already attached on the remote before the drop. We combine this with mosh for packet-loss-heavy links, though mosh requires UDP which some firewalls block.
OpenBSD's sshd configuration at /etc/ssh/sshd_config defaults to `ClientAliveInterval 0`, meaning the server sends no keepalives. Set `ClientAliveInterval 15` and `ClientAliveCountMax 3` on remote hosts to detect dead connections within 45 seconds rather than waiting for TCP timeout.
# In ~/.tmux.conf: send prefix to nested session
bind a send-prefix
# SSH config for reliable tmux sessions: ~/.ssh/config
Host openbsd-prod
HostName 198.51.100.45
User deploy
ServerAliveInterval 15
ServerAliveCountMax 3
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
# On the remote OpenBSD host: /etc/ssh/sshd_config
# ClientAliveInterval 15
# ClientAliveCountMax 3
# Attach or create on remote in one command
ssh openbsd-prod -t 'tmux attach || tmux new-session -s main'
Copy mode, clipboard integration, and xclip alternatives
OpenBSD does not ship xclip or xsel. On a headless server this does not matter. On a workstation running xenodm and cwm, you need a different approach to get tmux copy-mode selections into the X clipboard.
xclip is available in packages (`pkg_add xclip`) but we prefer xdotool-free solutions. The tmux `copy-pipe` command pipes the selection to any command. On OpenBSD with X running, use `xclip -in -selection clipboard`. Without X, pipe to a file or use the system pasteboard abstraction if you are on a macOS client SSH'd into OpenBSD.
Copy mode keybindings: Enter copy mode with `C-a Enter` (from our config above), navigate with vi keys, press v to start selection, y to yank and exit. The selection goes into tmux's internal buffer. Access it with `C-a ]` to paste, or `tmux show-buffer` to print it to stdout.
For multi-buffer workflows, `tmux list-buffers` shows all yanked selections and `tmux choose-buffer` opens an interactive picker. This replaces clipboard managers for terminal-only workflows.
# In ~/.tmux.conf: copy to X clipboard on OpenBSD workstation
bind -T copy-mode-vi y send -X copy-pipe-and-cancel "xclip -in -selection clipboard"
# Without X: copy to file
bind -T copy-mode-vi y send -X copy-pipe-and-cancel "cat > /tmp/tmux-buffer"
# Inspect buffers from shell
tmux list-buffers
tmux show-buffer -b 0
tmux show-buffer -b 1
# Paste a specific buffer
tmux paste-buffer -b 0
# Interactive buffer chooser: C-a = (default binding)
# Or set explicitly:
bind = choose-buffer
Status bar customization without plugins
The tmux status bar runs shell commands via `#()` syntax. On OpenBSD, avoid slow commands here because the status bar refresh (every 5 seconds in our config) blocks the client thread briefly. `uptime`, `sysctl hw.sensors`, and `df` are all fast enough. Avoid anything that might block on network I/O.
The following status bar shows: session name, current window load average, disk usage on the root filesystem, and timestamp. It fits in 80 columns and gives actionable data at a glance. Colors use the standard 256-color palette with explicit codes rather than named colors, which are terminal-dependent.
For monitoring multiple metrics, consider splitting the status bar across left and right sections. Use `status-left-length` and `status-right-length` to prevent truncation. The default right-length of 40 cuts off hostnames longer than 15 characters plus a timestamp.
# Status bar with live metrics - no plugins required
set -g status-interval 5
set -g status-bg colour235
set -g status-fg colour248
set -g status-left-length 40
set -g status-right-length 80
# Left: session name
set -g status-left "#[fg=colour39,bold][#S]#[default] "
# Right: load, disk, time
set -g status-right \
"#[fg=colour245]load: #(sysctl -n vm.loadavg | awk '{print $2}') "\
"| df: #(df -h / | awk 'NR==2{print $5}') "\
"| #[fg=colour39]#H#[default] "\
"| #[fg=colour248]%Y-%m-%d %H:%M"
# Window status format
setw -g window-status-format " #I:#W "
setw -g window-status-current-format "#[fg=colour39,bold] #I:#W#[default] "
# Pane border colors
set -g pane-border-style fg=colour238
set -g pane-active-border-style fg=colour39
Security considerations on OpenBSD
tmux on OpenBSD runs under pledge(2) with the pledges `stdio rpath wpath cpath fattr unix getpw sendfd recvfd proc exec tty ps`. This is visible in the source at tmux.c. The practical implication: tmux cannot make outbound network connections from within its own process, which eliminates an entire class of supply-chain attack vectors that affect plugin managers on Linux systems.
Socket permissions matter. By default, tmux creates its socket at `/tmp/tmux-$(id -u)/default` with mode 0700. Only your user can attach. If you run tmux as root for system-level monitoring (we do not recommend this, but it happens), the socket is at `/tmp/tmux-0/default` and readable only by root unless you deliberately chmod it.
For shared sessions - letting a colleague watch your terminal for pair debugging - use `tmux -S /tmp/shared.sock new-session` and then `chmod 660 /tmp/shared.sock` and add their user to a shared group. This is the correct approach, not running tmux as a shared user account. On OpenBSD, set up the group with `groupadd tmuxshare` and add users with `usermod -G tmuxshare username`.
doas integration: if you frequently run `doas` commands, note that doas on OpenBSD does not persist credentials between panes. Each pane has its own tty. A doas authentication in pane 1 does not carry to pane 2. Plan your workflows accordingly - use `doas -s` to spawn a root shell in a dedicated pane rather than typing doas repeatedly.
# Check tmux socket location
tmux display-message -p '#{socket_path}'
# /tmp/tmux-1000/default
ls -la /tmp/tmux-1000/
# total 8
# drwx------ 2 deploy wheel 512 Aug 23 09:15 .
# drwxrwxrwt 9 root wheel 512 Aug 23 09:14 ..
# srwx------ 1 deploy wheel 0 Aug 23 09:15 default
# Shared session setup
groupadd tmuxshare
usermod -G tmuxshare deploy
usermod -G tmuxshare colleague
tmux -S /tmp/shared.sock new-session -s pair
chgrp tmuxshare /tmp/shared.sock
chmod 660 /tmp/shared.sock
# Colleague attaches read-only
tmux -S /tmp/shared.sock attach -t pair -r
Automating tmux session recovery after reboot
OpenBSD reboots cleanly and often after patch cycles. Unlike Linux systems with resurrection tools like tmux-resurrect, we use a lightweight rc.d-style approach: a shell script that starts tmux sessions as the target user via doas, called from /etc/rc.local or a user-level rc script.
The approach uses `tmux has-session` to skip recreation if the session exists (relevant on systems with long uptime), then calls your session startup scripts. For each service account that needs persistent tmux sessions, create a dedicated startup script and invoke it with `doas -u username`.
OpenBSD's base system includes cron. A @reboot cron entry for each user achieves the same result without touching /etc/rc.local, which keeps system-level and user-level concerns separated.
# crontab -e (as the target user)
@reboot /bin/sh -l -c '/home/deploy/bin/webserver-session' > /tmp/tmux-boot.log 2>&1
# Or in /etc/rc.local for system-wide startup:
# /usr/bin/doas -u deploy /bin/sh -l -c '/home/deploy/bin/webserver-session'
# Verify sessions running after reboot
/usr/bin/tmux -u ls 2>/dev/null || echo "No tmux sessions"
# Script to dump current session layout for documentation
tmux list-windows -a -F '#{session_name}:#{window_index} #{window_name} #{pane_index}/#{window_panes}'
Benchmarking and performance on OpenBSD
On our test server - an OpenBSD 7.6 amd64 VPS with 2GB RAM - we ran baseline measurements for tmux under load. With 10 sessions open, each containing 4 panes, tmux consumed 18MB RSS total. CPU usage during active log streaming (tail -F on a file receiving 500 lines/second) peaked at 0.8% on a single core.
The `status-interval 5` setting matters more than most people realize. At the default of 15 seconds, status bar commands like `df` and `sysctl` run every 15 seconds. Setting it to 1 second for a dashboard-style status bar with live counters will burn measurable CPU if those commands are slow. Keep status bar commands under 50ms execution time and status-interval at 5 or higher for production hosts.
Scroll performance in copy mode on large buffers (50,000 lines) is fast because tmux stores history in a ring buffer allocated at session start. The memory is reserved upfront. With `history-limit 50000` and 10 panes, expect approximately 40MB pre-allocated for history buffers alone at peak. Reduce to 10000 on memory-constrained systems.
tmux's rendering pipeline uses a damage-based redraw system. Only changed cells are retransmitted over SSH. On a 100ms latency link, this makes tmux significantly more responsive than plain SSH with a busy terminal application because only the diff is sent, not a full screen repaint.
# Measure tmux memory usage
ps aux | grep tmux
# deploy 12345 0.0 0.8 18432 16384 ?? S 9:15AM 0:02.31 tmux
# Count total panes across all sessions
tmux list-panes -a | wc -l
# Time a status bar command to check it's fast enough
time sysctl -n vm.loadavg
# real 0m0.003s - safe for status-interval 5
time df -h /
# real 0m0.008s - safe
time pkg_info | wc -l
# real 0m0.240s - too slow for status bar, use cached file instead