kmail.at
← learning

sysadmin · difficulty ◆◆

tmux — sessions that survive the disconnect

A server with no service file, sessions that outlive your SSH connection, and one default option that can kill them all at once.

The reason your import job is still running when you SSH back in is a server you never once attached to.

2026-10-06 · 6 min read

$ tmux

One server, then sessions, windows, panes — in that order

tmux has no daemon and no unit file. The first time you run the binary it forks a server process that owns a unix socket at /tmp/tmux-$(id -u)/default, and every session you create lives inside that one process. Clients attach to that socket; close the client and nothing else notices. The hierarchy is strict: the server owns sessions, a session owns windows, a window owns panes. That is why `tmux ls` works from a shell that has never attached to anything — it is just asking the socket a question. On this box `tmux -V` reports 3.4, and the server exposes 90 subcommands and 260 key bindings. I have used maybe twelve of them.

The -d flag is the entire trick

`-d` creates the session and hands your prompt straight back instead of attaching. Because nothing tries to draw a screen, `tmux new -d -s import './load_orders.sh'` works from cron, from a CI step, from a one-off SSH command, from anywhere — no TTY required. Your job now owns a pty and a parent process that outlives the shell you started it from. That is the whole pitch, and it is why I reach for tmux before nohup: nohup gives you a process with nowhere to type and no way to look at it.

The command line is the API

Every key binding has a subcommand, every subcommand takes `-t` to say which session, window or pane it means, and most of them take `-F` to print fields you choose. That is what turns tmux from a terminal toy into infrastructure: capture-pane reads a pane, send-keys types into one, pipe-pane tees one to a file, wait-for blocks a script until a pane signals. I use this to babysit long imports on machines where I can only hold one SSH connection at a time.

Example

$ tmux new -d -s ops -x 140 -y 30 'watch -n 5 "df -h /"'
tmux split-window -h -t ops -d 'tail -f /var/log/syslog'
tmux split-window -v -t ops -d 'journalctl -f -n 5'
tmux split-window -v -t ops -d 'watch -n 5 "free -m"'
tmux select-layout -t ops tiled
tmux ls
tmux list-panes -a -F '#{pane_index}  #{pane_width}x#{pane_height}  at #{pane_left},#{pane_top}  #{pane_current_command}'
ops: 1 windows (created Tue Oct  6 09:07:47 2026)
0  69x14  at 0,0  watch
1  70x14  at 70,0  watch
2  69x15  at 0,15  journalctl
3  70x15  at 70,15  tail

The first five commands print nothing at all, which is normal and quite the point. -x/-y fix the geometry so a session created with no terminal still has a sane size (140x30 became four panes of 69x14 and 70x15). -h splits side by side, -v splits top and bottom, -d leaves focus on the pane you were already in, and select-layout tiled evens the mess out. Real output on tmux 3.4.

$ tmux capture-pane -pt ops:0.0
tmux list-panes -a
Every 5.0s: df -h /                     nuc: Tue Oct  6 09:07:50 2026

Filesystem                         Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv  455G  409G   27G  94% /
ops:0.0: [69x14] [history 0/2000, 4999 bytes] %0 (active)
ops:0.1: [70x14] [history 0/2000, 5143 bytes] %3
ops:0.2: [69x15] [history 20/2000, 59203 bytes] %2
ops:0.3: [70x15] [history 28/2000, 68542 bytes] %1

-p prints the pane to stdout instead of dumping it into your own scrollback, -t names the target, and panes are addressed as session:window.pane. The default list-panes -a output shows the history buffer per pane: 2000 lines is the compiled-in history-limit, and both watch panes sit at 0/2000 because watch repaints in place instead of ever emitting a newline.

$ tmux new-window -t ops -n quick
tmux send-keys -t ops:quick 'uptime' Enter
sleep 1
tmux capture-pane -pt ops:quick
kmail@nuc:~/.hermes/cache/scratch$ uptime
 09:07:17 up 6 days, 20:24,  3 users,  load average: 0.75, 1.19, 1.30

Named keys (Enter, C-c, Escape, Up) are real key presses; add -l when your text merely looks like a key name. `tmux send-keys -t ops:0.2 C-c` is your Ctrl-C on a process you cannot see, and it is the fastest way to stop something that is about to fill a disk. The uptime and load figures are live values from this machine.

$ tmux new -d -s import './load_orders.sh'
tmux pipe-pane -t import 'cat >> /tmp/import.log'
sleep 12
ls -l /tmp/import.log
cat -v /tmp/import.log
tmux pipe-pane -t import        # no argument stops the pipe
-rw-rw-r-- 1 kmail kmail 80 Oct  6 09:03 /tmp/import.log
09:03:19  imported batch 002 (25 rows)^M
09:03:24  imported batch 003 (25 rows)^M

That is a real 12-second run against a script that prints one timestamped line every 5 seconds — pipe-pane only sees text a program writes as lines. Pointed at a full-screen redrawer instead (my first attempt used the df watch pane) it produced a 0-byte file over eleven seconds, because watch moves the cursor and repaints rather than printing. Strip the ^M with `tr -d '\r'` before feeding the log to anything: a pty ends its lines with CRLF.

$ tmux has-session -t ops 2>/dev/null && echo "ops already running - reusing it" || tmux new -d -s ops
tmux has-session -t nope 2>/dev/null; echo "has-session nope rc=$?"
tmux new -d -s job 'sleep 3; tmux wait-for -S sync-done'
time tmux wait-for sync-done
ops already running - reusing it
has-session nope rc=1

real	0m3.011s
user	0m0.001s
sys	0m0.002s

has-session exits 1 when the session is missing, which is exactly what a || guard wants — send its stderr ("can't find session: nope") to /dev/null. wait-for is the handshake: -S name sets it from inside the pane, plain `wait-for name` blocks in your calling script until it is set. Start the job, block until the pane signals, then send the report — no sleep-and-hope.

Common flags

-d (new-session)
Create the session and return to your prompt instead of attaching. Needs no TTY at all — this is why cron jobs and CI steps can use it.
-s name / -t target
Name a session, then aim at it. Targets read session, session:window, session:window.pane — `-t ops:0.2` is pane 2 of window 0. A session target alone means its active pane.
-A (new-session)
Attach if the session exists, create it if it does not: one idempotent line. It does need a terminal — from a script it fails with 'open terminal failed: not a terminal'.
-F '#{...}'
Print your own fields instead of the default line. An unknown variable expands to the empty string rather than erroring, so check the name with `display-message -p '#{session_count}'` first.
capture-pane -p -S - -J
-p writes to stdout, -S - starts at the oldest line in history (2000 by default), -J re-joins lines the pane wrapped. Add -e to keep the SGR colour escapes.
split-window -h | -v -d -l 40%
Split side by side versus top and bottom, -d keeps focus where it is, -l sizes the new pane in lines or as a percentage. Chained with -t, four commands build a layout.
send-keys / pipe-pane
Type into a pane — named keys like Enter, C-c, Escape, or -l for literal text — and tee everything that pane prints into a command such as 'cat >> file'.

History

The prototype was developed inside GNU screen

The upstream FAQ answers its own question 'Why is C-b the prefix key?' with: 'The default key is C-b because the prototype of tmux was originally developed inside screen and C-b was chosen not to clash with the screen meta key.' The oldest entry in the CHANGES file is dated 09 July 2007 and reads 'Initial import to CVS. Basic functions are working, albeit with a couple of showstopper memory bugs and many missing features' — no status bar, no key remapping. Nicholas Marriott kept it portable, ported it to OpenBSD (where it ships in the base system) and licensed it under the ISC licence; modern flags arrived gradually, with new-session -A and resize-pane -Z landing together in 1.7 on 13 October 2012.

Version numbers that mean nothing on purpose

Release cadence is deliberately boring: roughly every six months, around OpenBSD releases, because tmux is part of OpenBSD. Patch releases get a trailing letter (3.1a), the development tree is prefixed 'next-', release candidates carry '-rc', and the copy bundled in OpenBSD reports the OS version prefixed with 'openbsd-'. The installed build here is 3.4, while the master changelog already opens with 'CHANGES FROM 3.7c TO 3.8'. The FAQ says it plainly: 'tmux 3.0 is the release after tmux 2.9, nothing more.'

Fun facts

Pros & cons

pros

  • + Sessions outlive the thing that started them: drop the VPN, close the laptop, come back and `tmux attach -t ops` finds the panes exactly where they were.
  • + You get a full operations wall on a machine with no display, assembled from non-interactive one-liners you can keep in a script or a runbook.
  • + Every interactive action has a subcommand with -t targeting and -F formatting, so cron and CI can drive it with no terminal anywhere in sight.

cons

  • − The server dies with its last session, so it is not a process supervisor — one session whose command returns immediately can end the whole thing.
  • − The prefix key swallows C-b inside every program you run in a pane; Emacs users get bitten within about four minutes.
  • − capture-pane and pipe-pane only understand text a program writes as lines: watch, top and friends repaint with cursor moves and log almost nothing — 0 bytes over eleven seconds in my test.

Takeaways

  1. 1`tmux new -d -s name 'cmd'` then `tmux attach -t name` replaces nohup, screen and a systemd unit in one line.
  2. 2Verify from the outside with `tmux ls` or `tmux list-panes -a -F '#{pane_index} #{pane_current_command}'` — a tmux command that succeeds prints nothing.
  3. 3Guard your startup with `tmux has-session -t name 2>/dev/null || tmux new -d -s name 'cmd'` so a second run reuses the session instead of failing.
  4. 4Never leave a detached session running a command that exits immediately; with exit-empty on, losing the last session takes the server with it.
  5. 5Scrape panes with `capture-pane -p -S - -J`, log them with `pipe-pane 'cat >> file'`, and keep `tmux kill-server` for the day you want your CPU and your children back.

Related commands

← all learning