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.

Your build is running
iPhone Blink · mosh battery 0% MacBook · the host launchd / systemd sshd login shell tmux client npm run build SIGHUP · branch dies tmux server shell herdr server npm run build unix socket — not a parent/child link — build running · 42% connection gone · the build never noticed reattached · 91% · nothing was lost SIGHUP · build killed at 47% reconnect → start again from 0%
  1. ConnectedYou are attached from the phone. Note the shape of the tree: sshd owns 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 of launchd, not of anything on the left.
  2. ConnectedNo multiplexer. You typed npm run build at the shell sshd gave you, so the build is a grandchild of sshd. Everything you care about hangs off the connection.
  3. 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 sshd to notice the socket is dead.
  4. 2 · sshd cleans up after itselfsshd sends SIGHUP to 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.
  5. 2 · sshd cleans up after itselfThe same SIGHUP goes 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.
  6. 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.
  7. 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.
Only the left branch dies. Because your work hangs off the tmux server rather than off 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.

ActionCommand or keys
Attach if exists, else createtmux new -A -s phone
List sessionstmux ls
Detach (leave it running)Ctrl-b then d
New windowCtrl-b c
Next / previous windowCtrl-b n / p
Split vertically / horizontallyCtrl-b % / "
Scrollback / copy modeCtrl-b [ — then arrows, q to exit
Kill a sessiontmux 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

~/.tmux.conf
# 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
Make the session start automatically

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.

Five agents, watched by
Five agents, and one of them is waiting on you Herdr reads each pane and labels it tmux shows you rectangles and wishes you luck 1 · api-refactor claude · editing files 2 · flaky-test claude · running the suite 3 · db-migration claude · schema change 4 · docs-pass claude · rewriting README 5 · deploy-script claude · terraform plan running? running? running? running? running? working working working working working working working blocked working working done working blocked working blocked done working working working working tmux · [1] agents* [2] shell you open pane 1 · working · not this one Ctrl-b n → pane 2 · working · not this one either Ctrl-b n → pane 3 · there it is, four keypresses later …and pane 5 has been waiting for two minutes without your knowing herdr · 5 panes · all working nothing needs you. Put the phone down. herdr · 1 blocked — pane 3 · db-migration prefix + g takes you straight there. You did not have to go looking. herdr · 2 blocked — panes 3, 5 · 1 done the whole queue is legible without opening a single pane herdr · both answered · 4 working, 1 done you touched exactly the two panes that needed you, and nothing else
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 + g takes you to whichever one you pick.
  6. 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.
  7. 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.
  8. 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.
Herdr's one real idea is reading the pane. Everything else it does — workspaces, tabs, detach, reattach — tmux already did. What tmux cannot do is tell you which of your agents is blocked, because tmux deliberately treats a pane's contents as none of its business. Whether that idea is worth a second multiplexer depends entirely on how many agents you run at once.
# 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
~/.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
ActionDefault binding
Help panelprefix + ?
Go to workspace / paneprefix + g
New tabprefix + c
Next / previous tabprefix + n / p
Split horizontallyprefix + -
Focus pane leftprefix + 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.

Config & keypress
Where does the keystroke land? both multiplexers answer to Ctrl-b Herdr remapped to Ctrl-a · you want a Herdr tab Herdr remapped to Ctrl-a · you want to detach tmux — running inside the outer tmux — you press Ctrl-b Ctrl-a outer tmux prefix = Ctrl-b inner herdr prefix = Ctrl-b prefix = Ctrl-a agent pane claude mine → consumed mine → consumed not mine → passed on never notified mine → new tab untouched, still running Ctrl-b c opened a tmux window — not a Herdr tab. Herdr looks broken. It is not. It is simply never being spoken to. Herdr opens a new tab, exactly as documented. One prefix per layer, and both layers stay reachable. tmux detaches the whole stack. Herdr and every agent under it keep running, waiting for you to come back.
  1. 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 press Ctrl-b then c.
  2. The stack, after remappingHerdr's config now says prefix = "ctrl+a". You want a new Herdr tab, so you press Ctrl-a then c.
  3. 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-b then d.
  4. 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.
  5. 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.
  6. 2 · tmux passes it throughCtrl-a means 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.
  7. 2 · tmux swallows it — correctlySame mechanism as the broken case, but now it is what you wanted. Ctrl-b belongs to tmux and only tmux, so the outer layer acting on it is right.
  8. 3 · Herdr never hears youEvery Herdr binding appears dead, and the failure is quiet: Ctrl-b c does not error, it opens a tmux window. You end up with tmux windows you did not want while wondering why Herdr is broken.
  9. 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.
  10. 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.
Give each multiplexer its own prefix. Change Herdr's rather than tmux's — tmux muscle memory is worth preserving, and Herdr's config file makes the change trivial. Keep the convention identical on every machine or you will fight it constantly.
Should you nest at all?

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.

Herdr is young and moving fast

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.conf in 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+b if nesting under tmux.
  • resume_agents_on_restore enabled 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.