Chapter 04 · Persistence
tmux & Herdr
Mosh keeps your session alive while the network misbehaves. It cannot help when the phone runs out of battery. For that you need the work to be running somewhere that does not care whether anyone is watching — which is precisely what a terminal multiplexer is.
Why detaching works at all
The insight is about process parentage. Normally your shell is a child of sshd: kill
the connection and the shell is orphaned and killed with it. tmux breaks that chain. Your shell
becomes a child of the long-lived tmux server, which is not attached to any
terminal at all. The tmux client is just a viewer that connects to the server over a
local socket, and viewers are disposable.
- ConnectedYou are attached from the phone. Note the shape of the tree:
sshdowns a login shell which owns a tmux client, and that client talks to the tmux server over a unix socket. The server is a child oflaunchd, not of anything on the left. - ConnectedNo multiplexer. You typed
npm run buildat the shellsshdgave you, so the build is a grandchild ofsshd. Everything you care about hangs off the connection. - 1 · The phone diesBattery flat, or a tunnel, or airplane mode. The TCP connection is gone. Nothing on the host has reacted yet — it takes a moment for
sshdto notice the socket is dead. - 2 · sshd cleans up after itself
sshdsendsSIGHUPto its session and the whole left branch exits: the login shell, and with it the tmux client. The socket closes. That is the entire blast radius. - 2 · sshd cleans up after itselfThe same
SIGHUPgoes out — but this time your build is inside that branch, so it is killed along with the shell. Forty-seven percent of the way through, and gone. - 3 · Reattach wheneverThe tmux server never had a terminal to lose, so it and everything under it kept running. Later you connect from anywhere, a fresh client attaches to the same server, and the screen comes back mid-build. A viewer died; the work did not.
- 3 · Nothing to come back toThere is no server holding the work, so there is nothing to reattach to. You reconnect, you get a brand new shell, and you start the build again from zero.
sshd, losing the connection destroys a viewer and nothing else. A build started
on the train is still running when you reattach from your desk.
tmux, the parts you need
The hierarchy is session → window → pane. A session is the persistent unit; a window is a tab; a pane is a split. You need surprisingly few commands.
| Action | Command or keys |
|---|---|
| Attach if exists, else create | tmux new -A -s phone |
| List sessions | tmux ls |
| Detach (leave it running) | Ctrl-b then d |
| New window | Ctrl-b c |
| Next / previous window | Ctrl-b n / p |
| Split vertically / horizontally | Ctrl-b % / " |
| Scrollback / copy mode | Ctrl-b [ — then arrows, q to exit |
| Kill a session | tmux kill-session -t phone |
Copy mode matters more than usual here: since Mosh has no scrollback of its own, tmux's copy mode is your scrollback.
A phone-friendly config
# Mouse on — genuinely useful when your input device is a thumb.
set -g mouse on
# Big scrollback, since Mosh provides none.
set -g history-limit 50000
# Renumber windows when one closes, so the numbers stay small and tappable.
set -g renumber-windows on
# Colour and undercurl passthrough for modern terminals.
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",*256col*:Tc"
# Don't wait after Escape — Vim feels broken otherwise.
set -sg escape-time 10
# Keep the status bar informative but narrow enough for a phone screen.
set -g status-right "#{?client_prefix,PREFIX ,}#S "
set -g status-left ""
# Index from 1: reaching '0' on a phone keyboard is a nuisance.
set -g base-index 1
setw -g pane-base-index 1
Set your Blink host's command to tmux new -A -s phone. You never type
tmux again — connecting is attaching, and a dropped connection followed by
a reconnect lands you exactly where you were.
Herdr
Herdr is a terminal multiplexer built specifically for running coding agents. It is a single Rust binary with a background server, and it borrows tmux's shape — workspaces, tabs, panes, detach and reattach — while adding the thing tmux has no concept of: it reads each pane and knows whether the agent inside is working, blocked, or idle.
That distinction is the entire value proposition. With five agents running, tmux shows you five identical rectangles and you cycle through them hunting for the one waiting on your approval. Herdr tells you which one it is.
- All five are workingHerdr watches what each pane is actually printing, so it can say so. The useful part here is the negative result: you can see at a glance that nothing needs you, which is the state you want to be able to trust.
- All five are… somethingtmux knows a process is attached to a pty. It does not read the pane, so it has nothing to report. To learn anything you have to open a pane and look.
- 1 · Pane 3 stops and asksThe migration agent hits a destructive step and waits for approval. Herdr notices the pane went quiet on a prompt and flags it. You find out without having gone looking.
- 1 · Pane 3 stops and asks — silentlyThe same thing happens, and tmux's status bar looks exactly as it did a second ago. You start cycling:
Ctrl-b n, look,Ctrl-b n, look. - 2 · A second one blocks, a first one finishesPane 5 hits an approval too, and pane 1 is done. Three different states across five panes, all readable at once.
prefix + gtakes you to whichever one you pick. - 2 · You finally find pane 3Four keypresses in, you land on the one that was waiting. It has been idle that whole time. Meanwhile pane 5 has quietly blocked behind you and pane 1 finished without your noticing.
- 3 · You answer both and stopTwo approvals, two visits, nothing else opened. That is the difference the state-reading buys you: attention spent where it was needed rather than spread evenly over five panes.
- 3 · You keep cyclingHaving answered pane 3, you have no way to know pane 5 is waiting except to keep walking the list. With five agents that is tolerable. It scales badly.
# Install (macOS and Linux)
brew install herdr
# or
curl -fsSL https://herdr.dev/install.sh | sh
# Start it
herdr
# Write out the defaults as a starting point for your own config
herdr --default-config > ~/.config/herdr/config.toml
[keys]
# See the collision warning below — do not leave this as ctrl+b if
# you intend to run Herdr inside tmux.
prefix = "ctrl+a"
[session]
# Bring agent conversations back after the Herdr server restarts.
resume_agents_on_restore = true
[terminal]
default_shell = "zsh"
[theme]
name = "catppuccin"
auto_switch = true
| Action | Default binding |
|---|---|
| Help panel | prefix + ? |
| Go to workspace / pane | prefix + g |
| New tab | prefix + c |
| Next / previous tab | prefix + n / p |
| Split horizontally | prefix + - |
| Focus pane left | prefix + h |
The nesting problem
Running Herdr inside tmux hits an immediate and confusing wall: both default to
Ctrl-b. The outer multiplexer grabs the keystroke and the inner one never
sees it. Every Herdr binding appears dead, and it looks like Herdr is broken when it is simply
never being spoken to.
- The stack, with the defaultsBlink → mosh → tmux → Herdr → your agent pane. Both multiplexers were installed with their factory prefix, so both are sitting there listening for
Ctrl-b. You want a new Herdr tab, so you pressCtrl-bthenc. - The stack, after remappingHerdr's config now says
prefix = "ctrl+a". You want a new Herdr tab, so you pressCtrl-athenc. - The stack, after remappingSame config, different intent: this time you want to detach the outer tmux and put the phone away, so you press
Ctrl-bthend. - 1 · The outermost layer sees it firstThat is all "outer" means. Every byte you type is delivered to tmux, and only tmux decides whether anything further in gets a copy. Herdr is downstream of that decision and cannot influence it.
- 2 · tmux swallows itThe keystroke matches tmux's own prefix, so tmux consumes it and waits for the next key to complete a tmux command. Nothing is written to the pane, so nothing reaches Herdr.
- 2 · tmux passes it through
Ctrl-ameans nothing to tmux, so it is treated as ordinary input and written straight into the pane — and the program running in that pane is Herdr. - 2 · tmux swallows it — correctlySame mechanism as the broken case, but now it is what you wanted.
Ctrl-bbelongs to tmux and only tmux, so the outer layer acting on it is right. - 3 · Herdr never hears youEvery Herdr binding appears dead, and the failure is quiet:
Ctrl-b cdoes not error, it opens a tmux window. You end up with tmux windows you did not want while wondering why Herdr is broken. - 3 · Herdr actsHerdr sees its own prefix, opens a tab, and tmux is none the wiser. Both layers keep their full keymap because the two prefixes never collide.
- 3 · tmux actsThe outer session detaches and you close the phone. Herdr, its panes, and every agent inside are children of the Herdr server, which is a child of a shell inside the tmux server — so they all keep running.
Be aware of the redundancy: Herdr already runs a background server and already survives disconnection, so wrapping it in tmux duplicates that. The honest reasons to nest anyway are that tmux is the older, more battle-tested reattach layer, that it gives you scrollback which Mosh lacks, and that it holds your non-agent shells alongside the agent workspace. If you do not need those, running Herdr directly under Mosh is simpler and one less prefix to remember.
The nested layout
# From Blink
mosh macbook -- tmux new -A -s phone
# Inside tmux: window 1 for agents, window 2 for an ordinary shell
herdr # ctrl-b c for a second window whenever you want one
Detaching from the outer tmux with Ctrl-b d leaves Herdr and every agent running underneath. Reconnecting drops you straight back in.
Keybindings and config keys are still settling. Treat the tables above as a starting point and
check herdr --default-config and prefix + ? on the version you have
actually installed rather than trusting any written guide, including this one.
Checklist
- tmux installed on every machine you connect to.
~/.tmux.confin place with mouse mode and a large history limit.- Blink host command set to
tmux new -A -s phone. - Verified that a long-running process survives killing the client.
- Herdr installed and started once interactively.
- Herdr's prefix remapped away from
ctrl+bif nesting under tmux. resume_agents_on_restoreenabled if you want agents back after a restart.
The commands in this guide change firewall and login settings, and can lock you out of a machine. Practise on something disposable first. Everything here is provided as is, with no warranty — you accept the risk of running it. Read the disclaimer.