{"slug": "herdr-persistent-ai-agent-workspace-setup-for-macos-reboot-safe-layout-claude", "title": "Herdr: persistent AI agent workspace setup for macOS — reboot-safe layout + Claude/OpenCode session resume", "summary": "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.", "body_md": "A working configuration for [Herdr](https://herdr.dev) that survives a reboot: shut the\nmachine down, boot it back up, and every workspace, tab, pane **and running AI agent\nconversation** comes back where you left it.\n\nVerified end-to-end on macOS with Herdr 0.7.4, Claude Code, and OpenCode.\n\nOut of the box, Herdr persists your **layout** (workspaces, tabs, panes, cwd, focus) across\na server restart. It does **not** automatically resume your agent *conversations* unless you\ninstall the agent integrations. Without them you reboot and get your panes back as empty\nshells, with every Claude/OpenCode session orphaned.\n\nThe single highest-value step in this guide is step 1.\n\nHerdr resumes an agent only if it has a **session ref** for that pane. Session refs come from\nofficial integrations — hooks/plugins that the agent runs and that report its session ID back\nto Herdr over a socket. Screen detection alone is not enough: Herdr will *show* the agent in\nthe sidebar but have no idea how to relaunch it.\n\n```\nherdr integration install claude\nherdr integration install opencode\nherdr integration status\n```\n\nExpected:\n\n```\nclaude:   current (v7)  (~/.claude/hooks/herdr-agent-state.sh)\nopencode: current (v8)  (~/.config/opencode/plugins/herdr-agent-state.js)\n```\n\n`herdr integration list`\n\nshows everything available: pi, codex, copilot, devin, droid, kimi,\nkilo, hermes, qodercli, cursor, mastracode, omp.\n\n**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`\n\n,`session.updated`\n\n, 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`\n\nfails with`pi extension directory not found`\n\nif 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.\n\n``` python\npython3 -c \"\nimport json\nd=json.load(open('$HOME/.config/herdr/session.json'))\nfor w in d['workspaces']:\n    for ti,t in enumerate(w['tabs']):\n        for pid,p in t['panes'].items():\n            s=p.get('agent_session')\n            tab=t.get('custom_name') or f'tab{ti+1}'\n            print(f\\\"{(w.get('custom_name') or '(unnamed)'):18} {tab:18} pane {pid} -> \\\" +\n                  (f\\\"{s['agent']:9} {s['value'][:16]} src={s['source']}\\\" if s else '(none)'))\n\"\n```\n\nYou want `src=herdr:claude`\n\n/ `src=herdr:opencode`\n\n. That prefix means the *integration*\nreported it. A pane with no `agent_session`\n\ncomes back as a plain shell.\n\n```\nonboarding = false\n\n[ui]\nagent_panel_sort = \"spaces\"\n\n[session]\n# Resume claude/opencode panes into their native conversations after a restart.\n# Requires the agent integrations (herdr integration status).\nresume_agents_on_restore = true\n\n[keys]\n\n# Shut the whole server down cleanly before a reboot (not just detach).\n[[keys.command]]\nkey = \"prefix+alt+q\"\ntype = \"shell\"\ncommand = \"/Users/YOU/.local/bin/herdr server stop\"\n\n[ui.toast]\ndelivery = \"terminal\"\n\n[theme]\nname = \"catppuccin\"\nauto_switch = false\n```\n\nSubstitute your own username in the `command`\n\npath — use an absolute path, since the command\nruns detached and may not inherit your interactive PATH.\n\nApply without restarting:\n\n``` js\nherdr server reload-config\n# => {\"result\":{\"diagnostics\":[],\"status\":\"applied\",\"type\":\"config_reload\"}}\n```\n\nNon-empty `diagnostics`\n\nmeans a config error. See all available options with\n`herdr --default-config`\n\n.\n\nDefaults to `true`\n\n, but pin it explicitly so a future default change can't silently break\nyour setup.\n\nReplays recent terminal output after a restart, so panes look like you left them rather than\ncoming back blank. Leave it disabled unless you accept the tradeoff: it writes raw pane\nscrollback to `~/.config/herdr/session-history.json`\n\nin **plaintext**, including anything that\nscrolled past (prompts, command output, tokens or keys you echoed).\n\nTo turn it on, add to `config.toml`\n\n:\n\n```\n[experimental]\npane_history = true\n```\n\nThen harden it, because a one-shot `chmod 600`\n\non the history file does **not** hold. Herdr\nrewrites the file as you work, and an atomic write (temp file plus rename) creates a new inode\nat the server's umask, silently restoring the world-readable `644`\n\non the next save. Lock the\n**directory** instead, which Herdr never recreates:\n\n```\nchmod 700 ~/.config/herdr    # denies other local users regardless of any file's own mode\n```\n\nFor the file to be owner-only from creation, also give the Herdr server a restrictive umask:\nadd `<key>Umask</key><integer>63</integer>`\n\n(decimal for octal `077`\n\n) to the LaunchAgent in\nstep 3.\n\nKnow the limit of all of this. Permissions only stop *other local users*. The scrollback is\nstill plaintext at rest, so it flows into Time Machine and any backup and stays readable by\nanything already running as you. If real secrets pass through these panes (cloud credentials,\nDB sessions, API tokens), do not enable `pane_history`\n\nat all. No chmod makes plaintext secrets\non disk safe.\n\nThere is **no built-in \"quit everything\" binding**. This is the most common point of\nconfusion:\n\n| Action | Key | Effect |\n|---|---|---|\n| Detach | `ctrl+b q` |\nServer keeps running. Every agent keeps running. Reattach and nothing was lost. |\n| Stop | `ctrl+b alt+q` (the binding above) |\nServer stops, all panes/agents exit, layout is saved. |\n\n`prefix+q`\n\nis **detach**, not quit. Detaching before a reboot does nothing useful — macOS\nkills the server anyway, just less cleanly.\n\nOn macOS, use the **left** Option key for `alt`\n\n. WezTerm (and most terminals) send left Option\nas Alt while right Option composes characters like `œ`\n\n. If the chord proves flaky, rebind to\n`prefix+shift+q`\n\n— no Alt involved.\n\nHerdr installs no LaunchAgent. After a reboot nothing runs until you type `herdr`\n\n. To have the\nsession already restored by the time you open a terminal:\n\n`~/Library/LaunchAgents/dev.herdr.server.plist`\n\n```\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>Label</key>\n\t<string>dev.herdr.server</string>\n\n\t<key>ProgramArguments</key>\n\t<array>\n\t\t<string>/Users/YOU/.local/bin/herdr</string>\n\t\t<string>server</string>\n\t</array>\n\n\t<key>RunAtLoad</key>\n\t<true/>\n\n\t<!-- Deliberately false: `herdr server stop` must stay stopped. -->\n\t<key>KeepAlive</key>\n\t<false/>\n\n\t<key>WorkingDirectory</key>\n\t<string>/Users/YOU</string>\n\n\t<key>EnvironmentVariables</key>\n\t<dict>\n\t\t<key>PATH</key>\n\t\t<string>/Users/YOU/.local/bin:/Users/YOU/.opencode/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>\n\t</dict>\n\n\t<key>StandardOutPath</key>\n\t<string>/Users/YOU/.config/herdr/launchd.out.log</string>\n\t<key>StandardErrorPath</key>\n\t<string>/Users/YOU/.config/herdr/launchd.err.log</string>\n</dict>\n</plist>\nplutil -lint ~/Library/LaunchAgents/dev.herdr.server.plist\nlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.herdr.server.plist\n```\n\n** KeepAlive must be false.** With\n\n`true`\n\n, launchd instantly resurrects the server after\nyour shutdown hotkey, making a clean shutdown impossible.If a server is already running, the job exits 1 with `error: herdr server is already running`\n\nand leaves the running one alone. That's correct behavior, not a failure.\n\n**The PATH must list every agent's binary directory.** Herdr relaunches agents by bare name\n(\n\n`claude`\n\n, `opencode`\n\n), so a launchd-started server resolves them against *this*\n\n`PATH`\n\n, not\nyour interactive shell's. `claude`\n\nlives in `~/.local/bin`\n\n; OpenCode installs to\n`~/.opencode/bin`\n\n, which is easy to leave out here. Omit a directory and that agent's panes\ncome back as empty shells even when the integration and refs are correct.To test the launchd path without rebooting: stop the server, then\n`launchctl kickstart -k gui/$(id -u)/dev.herdr.server`\n\n.\n\nHerdr kills panes with `SIGHUP`\n\non shutdown. With default zsh settings, history is only\nflushed when a shell exits cleanly — so **every command since your last clean exit is lost on\nevery Herdr restart**.\n\nSymptom: `~/.zsh_history`\n\nhas a modification time hours old while you've been working the\nwhole time.\n\nAdd to `~/.zshrc`\n\n(not `.zprofile`\n\n— see below):\n\n```\nHISTFILE=~/.zsh_history\nHISTSIZE=50000\nSAVEHIST=50000\nsetopt INC_APPEND_HISTORY   # write each command to disk immediately\nsetopt EXTENDED_HISTORY     # record timestamps\nsetopt HIST_IGNORE_SPACE    # leading space keeps a command out of history\n```\n\n`SHARE_HISTORY`\n\nis the stronger alternative — it also live-imports other panes' commands into\nthe current shell. Some find that disorienting since history order shifts underneath them;\n`INC_APPEND_HISTORY`\n\ngives durability without the cross-talk.\n\nNote that zsh history is **global, not per-pane**. Up-arrow in any pane shows commands from\nevery pane, interleaved. Per-pane history would mean a separate `HISTFILE`\n\nper pane, trading\naway the shared history most people want.\n\nVerify:\n\n```\nzsh -i -c 'echo \"SAVEHIST=$SAVEHIST\"; [[ -o inc_append_history ]] && echo \"inc_append=on\"'\n```\n\nA very common misconfiguration is putting interactive setup in `.zprofile`\n\n:\n\n| File | Runs for |\n|---|---|\n`.zshenv` |\nevery shell, including non-interactive scripts — env vars only |\n`.zprofile` |\nlogin shells only — PATH exports belong here |\n`.zshrc` |\nevery interactive shell — aliases, completions, hooks, history |\n\nIt appears to work when everything you use happens to be a login shell (WezTerm, Terminal.app\nand Herdr panes all spawn login shells on macOS by default). But any non-login interactive\nzsh — a nested `zsh`\n\n, some editor terminals, some container exec contexts — silently loses\nyour aliases, completions and `direnv`\n\nhook.\n\nKeep `PATH`\n\nexports in `.zprofile`\n\n: they use the `export PATH=\"new:$PATH\"`\n\nprepend form, so\nrunning them in `.zshrc`\n\nre-prepends on every nested shell and grows PATH without bound. Add\n`typeset -U path PATH`\n\nnear the top of `.zprofile`\n\nto dedupe automatically.\n\nConfirm a shell is a login shell — `ps`\n\nshows argv[0] with a leading dash:\n\n```\nps -eo pid,ppid,args | grep zsh    # \"-zsh\" = login shell\nherdr                  # launch or attach\nctrl+b alt+q           # stop everything (before a reboot)\nherdr                  # bring it all back\n```\n\nUseful keys (defaults):\n\n| Key | Action |\n|---|---|\n`ctrl+b q` |\ndetach (leaves everything running) |\n`ctrl+b w` |\nworkspace picker |\n`ctrl+b c` |\nnew tab |\n`ctrl+b v` / `ctrl+b -` |\nsplit vertical / horizontal |\n`ctrl+b z` |\nzoom pane |\n`ctrl+b b` |\ntoggle sidebar |\n`ctrl+b ?` |\nhelp |\n`ctrl+b s` |\nsettings |\n\nSnapshot before stopping:\n\n```\ncp ~/.config/herdr/session.json /tmp/session-before.json\n```\n\nAfter `herdr`\n\ncomes back, the agents should have been relaunched with the exact session IDs:\n\n```\nps -eo pid,args | grep -E \"[c]laude --|[o]pencode --\"\nopencode --session ses_1a2b3c4d5e6f...\nopencode --session ses_7g8h9i0j1k2l...\nclaude --resume 11111111-2222-3333-4444-555555555555\nclaude --resume 66666666-7777-8888-9999-000000000000\n...\n```\n\nEach ID should match its pane's `agent_session`\n\nfrom the snapshot. That's the proof — Herdr\nbuilt those command lines from the persisted refs.\n\nServer lifecycle in the log:\n\n```\ngrep -iE \"shutdown|startup|persist.restore\" ~/.config/herdr/herdr-server.log | tail\nserver shutdown initiated\nherdr exiting  event=\"app.shutdown\"  outcome=\"completed\"\nsession restore evaluated  event=\"persist.restore\"  outcome=\"ok\"  workspaces=5\nherdr starting  event=\"app.startup\"  outcome=\"started\"\n```\n\n| Path | What |\n|---|---|\n`~/.config/herdr/config.toml` |\nconfiguration |\n`~/.config/herdr/session.json` |\npersisted layout + agent session refs |\n`~/.config/herdr/session-history.json` |\npane scrollback (if `pane_history = true` ) |\n`~/.config/herdr/herdr-server.log` |\nserver log |\n`~/.config/herdr/herdr.sock` |\nsocket API |\n\n| Command | What |\n|---|---|\n`herdr status` |\nclient + server version/state |\n`herdr --default-config` |\nfull annotated default config |\n`herdr server reload-config` |\napply config.toml live |\n`herdr server stop` |\nstop the server |\n`herdr integration status` |\nwhich integrations are installed |\n`herdr session list` |\nnamed sessions |\n`herdr update` |\nself-update |\n\nSession 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.\n\n**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's*running*, not that it will*resume*. Only`agent_session`\n\nin`session.json`\n\nmeans resumable.**Detach is not quit.**`prefix+q`\n\nleaves everything running.**The first restart after installing integrations won't resume.** Nothing had refs yet.**Herdr passes no extra flags to agents.** It relaunches with`claude --resume <id>`\n\nor`opencode --session <id>`\n\nand nothing else — no environment beyond`HERDR_PANE_ID`\n\n,`HERDR_SOCKET_PATH`\n\n,`HERDR_ENV`\n\n. 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's`Cmd+d`\n\n, iTerm, tmux), panes created that way are not in`session.json`\n\n, not restored, and not resumed. Inside a Herdr session, use Herdr's splits.", "url": "https://wpnews.pro/news/herdr-persistent-ai-agent-workspace-setup-for-macos-reboot-safe-layout-claude", "canonical_source": "https://gist.github.com/lasergoat/7f92d7c4ba72ee9889bb4b1fd6cd287a", "published_at": "2026-07-21 18:16:40+00:00", "updated_at": "2026-08-15 00:42:03.251510+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["Herdr", "Claude Code", "OpenCode", "macOS"], "alternates": {"html": "https://wpnews.pro/news/herdr-persistent-ai-agent-workspace-setup-for-macos-reboot-safe-layout-claude", "markdown": "https://wpnews.pro/news/herdr-persistent-ai-agent-workspace-setup-for-macos-reboot-safe-layout-claude.md", "text": "https://wpnews.pro/news/herdr-persistent-ai-agent-workspace-setup-for-macos-reboot-safe-layout-claude.txt", "jsonld": "https://wpnews.pro/news/herdr-persistent-ai-agent-workspace-setup-for-macos-reboot-safe-layout-claude.jsonld"}}