Yesterday I wrote that Herdr keeps all my sessions running. That’s true until I reboot. Today I installed an update, rebooted, and opened Herdr again. Every space and every tab was still there, each in the right folder. But every one of them showed a bare shell prompt. Fifteen agents, gone.
The conversations themselves weren’t lost. Claude Code saves every session as a transcript in ~/.claude/projects, and claude --resume <id> picks one up exactly where it stopped. What was lost is which session belonged in which pane. I had four sessions in the same folder, so the folder alone didn’t tell me.
I asked Claude to put them back. It got there, by matching transcripts to tabs on their folder and on what the conversation was about, but it took a while and some guessing. So I had it make sure the next time is boring.
This isn’t really a Herdr problem. tmux with tmux-resurrect brings back your panes and their folders too, and a fresh claude in them isn’t the conversation you had. The old Herdr TUI actually resumed agents by itself. The new desktop app doesn’t, at least not yet. Its docs say a lost terminal gets a shell in its last folder, and its original command is never run again. The maintainer doesn’t want Herdr to guess per agent how to resume it, which is what made it flaky in the old version. Once agents can report that themselves through the terminal status protocol, it will come back (herdr-core#15). Until then, this is my stopgap.
The fix starts before the reboot. Claude Code runs its SessionStart hooks whenever a session starts, is resumed or is cleared, and the hook gets the session id on stdin. So the hook writes down which session runs in which pane.
The pane needs a name that survives a reboot. In Herdr that’s the terminal address, which it hands every program as HC_TERMINAL_ID. In tmux with tmux-resurrect it’s the pane’s position, like work:2.1. Here’s the tmux version:
#!/bin/bash
# ~/.claude/hooks/remember-pane
# Remembers which Claude Code session runs in which tmux pane.
[ -n "$TMUX_PANE" ] || exit 0
pane=$(tmux display -p -t "$TMUX_PANE" '#S:#I.#P')
dir=~/.local/state/claude-panes
mkdir -p "$dir"
jq -c '{session: .session_id, cwd: .cwd, transcript: .transcript_path}' > "$dir/$pane.json"
And in ~/.claude/settings.json:
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/remember-pane" }] }
]
}
My own version also skips claude -p runs, and remembers whether the session was started with --dangerously-skip-permissions, so the resumed session starts the same way.
After a reboot, not every remembered session should come back. Some I had closed hours before, and those panes are a plain shell on purpose.
It turns out Claude Code writes one last line to its transcript when it gets killed. So a session that was still running at shutdown has a transcript that was written in the last minutes before the machine went down. And the journal knows when that was: the last line of the previous boot.
shutdown=$(journalctl -b -1 -n 1 -o short-unix --no-pager -q | cut -d. -f1)
for record in ~/.local/state/claude-panes/*.json; do
pane=$(basename "$record" .json)
IFS=$'\t' read -r session cwd transcript < <(jq -r '[.session, .cwd, .transcript] | @tsv' "$record")
# Written in the ten minutes before the shutdown: it was still running.
[ "$(stat -c %Y "$transcript")" -ge $((shutdown - 600)) ] || continue
tmux send-keys -t "$pane" "cd $(printf %q "$cwd") && claude --resume $session" Enter
done
That last write isn’t documented anywhere, so it may change one day. My version also checks that the pane really sits at a bare shell, and that the session isn’t already open somewhere else, before it types anything.
A command you have to remember after a reboot is a command you forget, so now it runs by itself. Omarchy has a post-boot hook that runs once the desktop is up. Mine starts the restore in the background. It waits until the Herdr server runs, which is when I open the app, puts everything back and sends a notification with how many agents came back.
To make it run once per boot and not on every login, it leaves a marker in $XDG_RUNTIME_DIR. That folder is emptied at every boot, so the marker means exactly “done since this boot”:
marker="$XDG_RUNTIME_DIR/claude-restore.done"
[ -e "$marker" ] && exit 0
touch "$marker"
There is also a skill that goes with it, for the case the script can’t handle: a pane without a record, like a session from before the hook existed. It tells the agent how to find the session by hand, the way Claude did it today: transcripts written right before the shutdown, matched to a pane by folder and by what the conversation is about.
Next time I reboot, I open Herdr, wait a few seconds, and every agent is back in its own pane, in the middle of the conversation where I left it.
Like this post? Follow me on X (@jankeesvw)