cd /news/developer-tools/herdr-persistent-ai-agent-workspace-… · home topics developer-tools article
[ARTICLE · art-97507] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Herdr: persistent AI agent workspace setup for macOS — reboot-safe layout + Claude/OpenCode session resume

A developer has documented a configuration for Herdr, a persistent AI agent workspace tool for macOS, that enables reboot-safe layout and session resume for Claude Code and OpenCode. The setup requires installing official agent integrations, which report session IDs to Herdr, and configuring the server to resume agent conversations on restore. The guide includes verification steps and config snippets, noting that integrations only tag sessions started after installation.

read9 min views2 publishedJul 21, 2026

A working configuration for Herdr that survives a reboot: shut the machine down, boot it back up, and every workspace, tab, pane and running AI agent conversation comes back where you left it.

Verified end-to-end on macOS with Herdr 0.7.4, Claude Code, and OpenCode.

Out of the box, Herdr persists your layout (workspaces, tabs, panes, cwd, focus) across a server restart. It does not automatically resume your agent conversations unless you install the agent integrations. Without them you reboot and get your panes back as empty shells, with every Claude/OpenCode session orphaned.

The single highest-value step in this guide is step 1.

Herdr resumes an agent only if it has a session ref for that pane. Session refs come from official integrations — hooks/plugins that the agent runs and that report its session ID back to Herdr over a socket. Screen detection alone is not enough: Herdr will show the agent in the sidebar but have no idea how to relaunch it.

herdr integration install claude
herdr integration install opencode
herdr integration status

Expected:

claude:   current (v7)  (~/.claude/hooks/herdr-agent-state.sh)
opencode: current (v8)  (~/.config/opencode/plugins/herdr-agent-state.js)

herdr integration list

shows everything available: pi, codex, copilot, devin, droid, kimi, kilo, hermes, qodercli, cursor, mastracode, omp.

Integrations only tag sessions started after install. Agents already running when you install get no ref and will not resume on the next restart. This costs you exactly one cycle.A ref appears after activity, not at launch. The OpenCode plugin reports on session events (session.created

,session.updated

, tool calls). An agent sitting idle at its prompt may not have registered yet. Send one message, then check.Herdr won't install for an agent that isn't present.herdr integration install pi

fails withpi extension directory not found

if pi isn't installed. That's intentional — install the agent first, then the integration. Don't pre-create the directory; these integrations are versioned and a pre-staged file goes stale.

python3 -c "
import json
d=json.load(open('$HOME/.config/herdr/session.json'))
for w in d['workspaces']:
    for ti,t in enumerate(w['tabs']):
        for pid,p in t['panes'].items():
            s=p.get('agent_session')
            tab=t.get('custom_name') or f'tab{ti+1}'
            print(f\"{(w.get('custom_name') or '(unnamed)'):18} {tab:18} pane {pid} -> \" +
                  (f\"{s['agent']:9} {s['value'][:16]} src={s['source']}\" if s else '(none)'))
"

You want src=herdr:claude

/ src=herdr:opencode

. That prefix means the integration reported it. A pane with no agent_session

comes back as a plain shell.

onboarding = false

[ui]
agent_panel_sort = "spaces"

[session]
resume_agents_on_restore = true

[keys]

[[keys.command]]
key = "prefix+alt+q"
type = "shell"
command = "/Users/YOU/.local/bin/herdr server stop"

[ui.toast]
delivery = "terminal"

[theme]
name = "catppuccin"
auto_switch = false

Substitute your own username in the command

path — use an absolute path, since the command runs detached and may not inherit your interactive PATH.

Apply without restarting:

herdr server reload-config

Non-empty diagnostics

means a config error. See all available options with herdr --default-config

.

Defaults to true

, but pin it explicitly so a future default change can't silently break your setup.

Replays recent terminal output after a restart, so panes look like you left them rather than coming back blank. Leave it disabled unless you accept the tradeoff: it writes raw pane scrollback to ~/.config/herdr/session-history.json

in plaintext, including anything that scrolled past (prompts, command output, tokens or keys you echoed).

To turn it on, add to config.toml

:

[experimental]
pane_history = true

Then harden it, because a one-shot chmod 600

on the history file does not hold. Herdr rewrites the file as you work, and an atomic write (temp file plus rename) creates a new inode at the server's umask, silently restoring the world-readable 644

on the next save. Lock the directory instead, which Herdr never recreates:

chmod 700 ~/.config/herdr    # denies other local users regardless of any file's own mode

For the file to be owner-only from creation, also give the Herdr server a restrictive umask: add <key>Umask</key><integer>63</integer>

(decimal for octal 077

) to the LaunchAgent in step 3.

Know the limit of all of this. Permissions only stop other local users. The scrollback is still plaintext at rest, so it flows into Time Machine and any backup and stays readable by anything already running as you. If real secrets pass through these panes (cloud credentials, DB sessions, API tokens), do not enable pane_history

at all. No chmod makes plaintext secrets on disk safe.

There is no built-in "quit everything" binding. This is the most common point of confusion:

Action Key Effect
Detach ctrl+b q
Server keeps running. Every agent keeps running. Reattach and nothing was lost.
Stop ctrl+b alt+q (the binding above)
Server stops, all panes/agents exit, layout is saved.

prefix+q

is detach, not quit. Detaching before a reboot does nothing useful — macOS kills the server anyway, just less cleanly.

On macOS, use the left Option key for alt

. WezTerm (and most terminals) send left Option as Alt while right Option composes characters like œ

. If the chord proves flaky, rebind to prefix+shift+q

— no Alt involved.

Herdr installs no LaunchAgent. After a reboot nothing runs until you type herdr

. To have the session already restored by the time you open a terminal:

~/Library/LaunchAgents/dev.herdr.server.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>dev.herdr.server</string>

	<key>ProgramArguments</key>
	<array>
		<string>/Users/YOU/.local/bin/herdr</string>
		<string>server</string>
	</array>

	<key>RunAtLoad</key>
	<true/>

	<!-- Deliberately false: `herdr server stop` must stay stopped. -->
	<key>KeepAlive</key>
	<false/>

	<key>WorkingDirectory</key>
	<string>/Users/YOU</string>

	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>/Users/YOU/.local/bin:/Users/YOU/.opencode/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
	</dict>

	<key>StandardOutPath</key>
	<string>/Users/YOU/.config/herdr/launchd.out.log</string>
	<key>StandardErrorPath</key>
	<string>/Users/YOU/.config/herdr/launchd.err.log</string>
</dict>
</plist>
plutil -lint ~/Library/LaunchAgents/dev.herdr.server.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.herdr.server.plist

** KeepAlive must be false.** With

true

, launchd instantly resurrects the server after your shutdown hotkey, making a clean shutdown impossible.If a server is already running, the job exits 1 with error: herdr server is already running

and leaves the running one alone. That's correct behavior, not a failure.

The PATH must list every agent's binary directory. Herdr relaunches agents by bare name (

claude

, opencode

), so a launchd-started server resolves them against this

PATH

, not your interactive shell's. claude

lives in ~/.local/bin

; OpenCode installs to ~/.opencode/bin

, which is easy to leave out here. Omit a directory and that agent's panes come back as empty shells even when the integration and refs are correct.To test the launchd path without rebooting: stop the server, then launchctl kickstart -k gui/$(id -u)/dev.herdr.server

.

Herdr kills panes with SIGHUP

on shutdown. With default zsh settings, history is only flushed when a shell exits cleanly — so every command since your last clean exit is lost on every Herdr restart.

Symptom: ~/.zsh_history

has a modification time hours old while you've been working the whole time.

Add to ~/.zshrc

(not .zprofile

— see below):

HISTFILE=~/.zsh_history
HISTSIZE=50000
SAVEHIST=50000
setopt INC_APPEND_HISTORY   # write each command to disk immediately
setopt EXTENDED_HISTORY     # record timestamps
setopt HIST_IGNORE_SPACE    # leading space keeps a command out of history

SHARE_HISTORY

is the stronger alternative — it also live-imports other panes' commands into the current shell. Some find that disorienting since history order shifts underneath them; INC_APPEND_HISTORY

gives durability without the cross-talk.

Note that zsh history is global, not per-pane. Up-arrow in any pane shows commands from every pane, interleaved. Per-pane history would mean a separate HISTFILE

per pane, trading away the shared history most people want.

Verify:

zsh -i -c 'echo "SAVEHIST=$SAVEHIST"; [[ -o inc_append_history ]] && echo "inc_append=on"'

A very common misconfiguration is putting interactive setup in .zprofile

:

File Runs for
.zshenv
every shell, including non-interactive scripts — env vars only
.zprofile
login shells only — PATH exports belong here
.zshrc
every interactive shell — aliases, completions, hooks, history

It appears to work when everything you use happens to be a login shell (WezTerm, Terminal.app and Herdr panes all spawn login shells on macOS by default). But any non-login interactive zsh — a nested zsh

, some editor terminals, some container exec contexts — silently loses your aliases, completions and direnv

hook.

Keep PATH

exports in .zprofile

: they use the export PATH="new:$PATH"

prepend form, so running them in .zshrc

re-prepends on every nested shell and grows PATH without bound. Add typeset -U path PATH

near the top of .zprofile

to dedupe automatically.

Confirm a shell is a login shell — ps

shows argv[0] with a leading dash:

ps -eo pid,ppid,args | grep zsh    # "-zsh" = login shell
herdr                  # launch or attach
ctrl+b alt+q           # stop everything (before a reboot)
herdr                  # bring it all back

Useful keys (defaults):

Key Action
ctrl+b q
detach (leaves everything running)
ctrl+b w
workspace picker
ctrl+b c
new tab
ctrl+b v / ctrl+b -
split vertical / horizontal
ctrl+b z
zoom pane
ctrl+b b
toggle sidebar
ctrl+b ?
help
ctrl+b s
settings

Snapshot before stopping:

cp ~/.config/herdr/session.json /tmp/session-before.json

After herdr

comes back, the agents should have been relaunched with the exact session IDs:

ps -eo pid,args | grep -E "[c]laude --|[o]pencode --"
opencode --session ses_1a2b3c4d5e6f...
opencode --session ses_7g8h9i0j1k2l...
claude --resume 11111111-2222-3333-4444-555555555555
claude --resume 66666666-7777-8888-9999-000000000000
...

Each ID should match its pane's agent_session

from the snapshot. That's the proof — Herdr built those command lines from the persisted refs.

Server lifecycle in the log:

grep -iE "shutdown|startup|persist.restore" ~/.config/herdr/herdr-server.log | tail
server shutdown initiated
herdr exiting  event="app.shutdown"  outcome="completed"
session restore evaluated  event="persist.restore"  outcome="ok"  workspaces=5
herdr starting  event="app.startup"  outcome="started"
Path What
~/.config/herdr/config.toml
configuration
~/.config/herdr/session.json
persisted layout + agent session refs
~/.config/herdr/session-history.json
pane scrollback (if pane_history = true )
~/.config/herdr/herdr-server.log
server log
~/.config/herdr/herdr.sock
socket API
Command What
herdr status
client + server version/state
herdr --default-config
full annotated default config
herdr server reload-config
apply config.toml live
herdr server stop
stop the server
herdr integration status
which integrations are installed
herdr session list
named sessions
herdr update
self-update

Session state saves are debounced roughly 5 seconds after a change, so a hard power-off costs at most the last few seconds of layout edits.

The agent sidebar lying by omission. A pane shows its agent via process detection even with zero integrations installed. Seeing OpenCode listed tells you it'srunning, not that it willresume. Onlyagent_session

insession.json

means resumable.Detach is not quit.prefix+q

leaves everything running.The first restart after installing integrations won't resume. Nothing had refs yet.Herdr passes no extra flags to agents. It relaunches withclaude --resume <id>

oropencode --session <id>

and nothing else — no environment beyondHERDR_PANE_ID

,HERDR_SOCKET_PATH

,HERDR_ENV

. If an agent comes back in an unexpected mode, Herdr isn't the cause.Terminal-level splits are invisible to Herdr. If your terminal has its own pane splitting (WezTerm'sCmd+d

, iTerm, tmux), panes created that way are not insession.json

, not restored, and not resumed. Inside a Herdr session, use Herdr's splits.

── more in #developer-tools 4 stories · sorted by recency
── more on @herdr 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/herdr-persistent-ai-…] indexed:0 read:9min 2026-07-21 ·