The problem: the agents are on my desk, I am not
Herdr is a terminal workspace manager for coding agents. It keeps interactive agents like Claude Code, Codex and pi each in their own pane, knows whether they are idle, working or blocked, and exposes a herdr CLI to control them. On my desktop I keep four of them alive: one Claude Code, two Codex, one pi on a cheap model. A Mac mini holds two more.
At the desk this is great. Away from it, I only have a phone, and:
- Claude Code's built-in Remote Control only covers Claude sessions. Codex and pi are out of reach.
- An SSH client over Tailscale works, but watching a terminal, switching panes and typing commands on a phone is miserable.
- What I actually want is to say "have Codex run the test suite" and wait for the result — not to operate a terminal remotely.
Feishu is the app that lives on my phone anyway, and Larkway already connects my local agents to Feishu. So the question became: can a Larkway agent drive the agents inside Herdr?
The approach: no new backend, teach the agent the tool
My first instinct was to add a herdr backend to Larkway and bind Feishu threads to Herdr panes one-to-one. It would work, but it hard-codes the usage: one bot per pane, and every change in the Herdr line-up means a config change.
I went the other way: a Larkway agent can already run Bash, so let it learn the herdr CLI. It discovers which agents exist right now, who is idle, which directory each one is in; it decides whom to dispatch to; it waits and collects the result itself. Larkway stays untouched, and the Herdr roster can change any time.
So I redefined one Larkway bot as a "steward": understand the goal, break it into tasks, do the simple ones itself, dispatch the rest to Herdr agents via the CLI, follow up, and report back. Its role is essentially the Claude Code main conversation you would have at your desk — only the entry point is Feishu.
Setup: three config changes, zero code
The Larkway bot subprocess and the Herdr server live on the same machine, so the local socket is enough. Three small things needed attention:
Install the herdr skill. Herdr ships an agent-readable skill;
herdr --skillprints it. Put it in~/.agents/skills/herdr/SKILL.mdso both the Claude Code and Codex backends can read it.Get past the skill's guard. The official skill first checks for
HERDR_ENV=1and tells the agent to stop if it is missing. A Larkway bot is not inside a pane, but the CLI works perfectly well from outside. AddHERDR_ENV=1to~/.larkway/.env; the bridge passes it to child processes.Allow the commands. Larkway runs Claude-backed bots in acceptEdits mode with a Bash allowlist. Add
Bash(herdr *)topermissions.allowExtrain~/.larkway/config.json, plusBash(ssh <host> *)if you want to reach a second machine.
# 1. skill $ mkdir -p ~/.agents/skills/herdr && herdr --skill > ~/.agents/skills/herdr/SKILL.md # 2. guard variable (bridge passes it to bot subprocesses) $ echo 'HERDR_ENV=1' >> ~/.larkway/.env # 3. allowlist (~/.larkway/config.json) "permissions": { "allowExtra": ["Bash(herdr *)", "Bash(ssh mac-mini *)"] } $ larkway stop && larkway start
Then the steward's role memory spells out the principles: you are a dispatcher outside Herdr; run herdr agent list first every time; pick by status, directory and type; make dispatched tasks self-contained; never interrupt a working agent; bring blocked questions back to the human.
Run 1: prove the path works
The first test was deliberately tiny: ask the pi agent inside Herdr what 1+1 is.
1. herdr agent list → work-pi is idle
2. herdr agent prompt work-pi "what is 1+1, one sentence" --wait
3. herdr agent read work-pi → work-pi answered: "1+1 equals 2."
Meanwhile, in the pi pane on the desktop, the question and answer really did appear. The path works.
Run 2: let the steward choose
The second test named no agent. It exercised the whole loop — discover, choose, dispatch, report: see who is available in Herdr, pick the lightest one to ask what day it is, and explain the choice.
Agents in Herdr right now (4, all idle)
| name | type | status |
|---|---|---|
| work-claude | claude | idle |
| work-codex | codex | idle |
| work-codex-2 | codex | idle |
| work-pi | pi | idle |
Why work-pi: a one-line question with no code and no project context. By the selection rules that goes to pi, keeping the heavier Claude / Codex agents free for real work.
One wrinkle worth noting: the first time I told it "do not run any commands" and pi replied that it cannot know the date. Once allowed to run date it answered correctly. Lesson: time questions require letting the agent read the system clock.
That last paragraph was discovered and written by the steward itself, and it later appended the lesson to its own knowledge base. That is exactly what I wanted: the skill gives the basics; the agent records the pitfalls as it hits them and avoids them next time.
Run 3: dispatch to Codex, read the evidence from a file
The third test was real dispatching: have an idle Codex write a hello.py in a temp directory (print the time and the Python version), run it, report the raw output, and say where the evidence came from.
Dispatched to work-codex (a Codex agent). It wrote and ran /tmp/herdr-out/hello.py; the output is a 2026-09-06 00:41:16 timestamp plus Python 3.11.4 version info, exit 0.
Evidence comes from the result file it wrote by convention, /tmp/herdr-out/work-codex-20260906-004050.md. I re-ran the script locally and got the same result.
The result file Codex left behind looks like this — source, command, raw output, exit code, nothing missing. The steward read this file, not the screen.
# hello.py ## raw output 2026-09-06 00:41:16.087130 3.11.4 (main, Jul 25 2023, 17:07:07) [Clang 14.0.3 ...] ## exit code 0
From @ to card, under a minute.
Pitfalls
1. Waiting for the wrong state: done is not idle
The first version of the memory told it to use --wait --until idle. When pi finished, Herdr marked it done (finished in the background, not yet seen in the UI), so the wait never saw idle and the Bash tool timed out after two minutes. The right call is plain --wait: it returns on idle, done or blocked. Also raise the Bash tool timeout to ten minutes, otherwise the bot times out, not the agent.
2. A bot restarting its own bridge kills itself
Midway I asked the steward to upgrade Larkway. It installed the new version, asked whether to restart the bridge, and I chose "restart in the background". The moment it ran larkway stop, the bridge and the steward's own process died together; the following larkway start never ran. Four bots were offline for a few minutes until I started it by hand.
Anything that touches the bridge, the Herdr server or the network must not go through this path. It is now a hard rule: a bot never restarts its own bridge; it hands the command to a human.
3. Second-hand information gets rationalised
After the bridge came back, the alarm the steward had scheduled fired, it checked the version, and reported that "the restart script hit a launchd race and launchd relaunched the process on its own". The result was right; the cause was invented. It had no evidence but produced a plausible explanation. In this chain an agent relays an agent, and each layer may add a little inference, so "state the evidence source" became a hard rule.
4. Screen scraping is not a return value
herdr agent read gives you rendered terminal text: status bars, progress spinners, the input box and leftovers from the previous turn, all mixed together. Fine for short answers, unusable for long ones. If the agent runs on the alternate screen, old lines never enter scrollback, and a larger --lines cannot bring them back.
What came out of it: the herdr-operator skill
All of the above went into one skill, written from the viewpoint "I am outside Herdr, dispatching a group of agents", at ~/.agents/skills/herdr-operator/. The steward's and the backup operator's memories only point to it. The essentials:
- Discover:
herdr agent listevery time; care about name, agent type, agent_status and cwd. A second machine isssh <host> herdr agent list, same commands. - Choose: only idle / done; prefer an agent whose cwd is already in the target project; code changes go to Claude or Codex, light Q&A to pi; independent tasks run in parallel.
- Dispatch:
herdr agent prompt <name> "<self-contained task>" --wait --timeout 590000. The task text carries the goal, constraints, directory, expected output and the delivery convention. - Delivery convention: the target agent writes its final conclusion to
/tmp/herdr-out/<name>-<timestamp>.mdand prints only the path on its last line. - Evidence priority: the file it wrote → the raw session transcript (Claude Code's jsonl can be located from the session id Herdr exposes; Codex only by cwd and time; pi has none) → screen scrape. Always name the source.
- blocked: read what it is asking first. Confirmations the owner already authorised are answered with
send-keys; open questions go back to the thread. - Hard rules: no
herdr server stop, no killing, no closing panes you did not open, no restarting your own bridge, never interrupt a working agent, conclusions only from evidence.
What this is good for, and what it is not
After an evening with it, my verdict: this is an asynchronous dispatch system, not a remote terminal.
| Scenario | Use | Why |
|---|---|---|
| Away from the desk, clear goal, willing to wait | Feishu → Larkway → Herdr | Covers every agent type, no SSH client, shares the desktop session so you can pick it up when you return. |
| Claude only, want native streaming and permission prompts | Claude Code Remote Control | Structured and real-time, but Claude only. |
| Need to watch the screen and iterate | SSH + Tailscale | When the agent pops choices or asks for confirmation, one round trip through Feishu takes a minute or two; the flow breaks. |
The three do not conflict. Daily dispatching goes through Feishu, deep Claude sessions through Remote Control, and SSH when I really need to watch.
Why I insisted on "teach the agent the tool"
Looking back, the key decision was not writing a Herdr backend for Larkway. A pipeline would be "more stable", but every new agent in Herdr, every change of project directory, every parallel dispatch would mean config or code changes. Once the agent knows the tool, those become its own judgement calls: who is free, who is in which directory, which one is cheapest — and it writes down what it learns when it hits a wall.
Larkway has always been a thin bridge: the bridge only does what the agent cannot (Feishu long connection, cards, process management); process and judgement belong to the agent. This time the "tool" just happened to be another group of agents instead of git and merge requests.
Build your own
- Install Herdr, start your usual agents in panes and name them (
herdr agent rename). - Install Larkway, create a bot, run
larkway start. - Apply the three config changes above: skill,
HERDR_ENV=1,allowExtra. - Write the bot's memory as a dispatcher, not an executor; leave command usage to the skill.
- @ it in a group, starting with "ask pi what 1+1 is".
Questions are welcome in the community group or on GitHub.