Herdr FAQ
Launch and drive coding agents through Herdr (>= 0.9.0) without losing
prompts. Command semantics: herdr --skill (the binary ships its own current doc and is the
tiebreaker for any dispute with this file). This file covers only what goes wrong and the
recipes that avoid it.
Read the Invariants, then go straight to your agent's section in Per kind - each one is a complete launch-to-first-prompt recipe. Everything after that is shared reference: Launch for pane and name mechanics, Shapes for composing commands, Drive for the turn loop, Failures for triage.
Invariants
- Exit 0 is submission, never completion.
agent promptwrites text and Enter as one ordered submission and reports success only after both land - that still does not prove a turn started.send-keysproves nothing at all: it can return{"type":"ok"}for keystrokes that never reach the pane. Confirm by effect: state moved, or the text is visible in the pane. - Screens no detection rule matches read as
idle. agy has no idle rule at all, so every agyidleis a guess. Read the screen before the first prompt, always. agent starttimeout = the child never launched. Bad flag, PATH, or a wrapper process.pane readhas the real error;pane process-infoand herdr's own message do not.idleis not "finished". A claude turn that spawns background shells or a background MCP task ends and reportsidlewhile that work runs on.--waittracks lifecycle state, not turns: it can be satisfied by a turn already in flight, or by the idle an interrupted turn produces a second later.- The driving harness is a second gate. Dangerous passthrough flags (
--dangerously-*,danger-full-access) get classifier-blocked, and so can ordinary brief content inlined into a shell command.sleep N; herdr ...polling is banned - use one backgroundedprompt, or wait on a file.
Per kind
Each section is self-contained: flags, pre-flight, launch, dialog handling, and the quirks that bite while the agent runs. Shared mechanics are in Launch and Shapes.
codex
P=$(herdr pane split --current --direction right --cwd "$D" --no-focus | jq -r .result.pane.pane_id)
herdr agent start cx1 --kind codex --pane "$P" --timeout 90000 -- --approve-for-me --no-alt-screen
herdr agent read cx1 --source detection --lines 40 # ALWAYS, before the first prompt
herdr agent prompt cx1 "Carry out $D/brief.md." --wait --timeout 1800000 &
Never --full-auto: removed in 0.15x, it surfaces as a bare timeout and can leave the pane
wedged in startup-pending. Both the trust-directory dialog and the startup update dialog are
detected, so an untrusted dir fails fast with agent_not_ready rather than eating the first
prompt - answer with send-keys, then wait --until idle done. The one-time
post-integration install hooks-review gate is NOT detected (WONTFIX); answer it once per
machine. Session ref binds at the first prompt for a fresh start, but an explicit
-- resume <id> is persisted at launch.
claude (alias claude-code)
# Pre-trust first - the folder-trust dialog IS detected, so an untrusted dir fails the start.
# Trust is INHERITED: a new directory under an already-trusted parent needs nothing at all.
CFG=/tmp/agentcfg; mkdir -p "$CFG" # isolated from your real config
printf '{"projects":{"%s":{"hasTrustDialogAccepted":true}}}' "$D" > "$CFG/.claude.json"
P=$(herdr pane split --current --cwd "$D" --no-focus --env CLAUDE_CONFIG_DIR="$CFG" \
| jq -r .result.pane.pane_id)
herdr agent start cl1 --kind claude --pane "$P" --timeout 90000 -- --model "$M"
herdr agent read cl1 --source detection --lines 40 # ALWAYS, before the first prompt
herdr agent prompt cl1 "Carry out $D/brief.md." --wait --timeout 1800000 &
- If you do hit a dialog, the safe key differs per dialog. The folder-trust dialog puts
the cursor on
No, exit, so a bareenterkills the agent - senddown enter. Its Bash-permission dialog puts the cursor on1. Yes. idlelies in a specific way here. A turn that spawns background shells ends and reportsidlewhile they run; background agents and background MCP tasks keep itworking, and a background MCP task can holdidlefor its whole 3-10 minute life.- Grey "next prompt suggestion" text in the composer reads as real unsubmitted text in a
plain read - disambiguate with
--format ansi(the suggestion carries the dim attributeESC[2m). - Session ref binds at launch. A Claude Code UI update can break detection
(
herdr server update-agent-manifests). Native-launcher installs run under a version-string process herdr cannot identify: launch viaHERDR_AGENT=claude exec <path>, thenagent rename. - Under a different
CLAUDE_CONFIG_DIRthe agent loads its own skills. Confirm it has this skill current before its first turn - a running session has already snapshotted the old text and cannot be fixed in place.
agy (aliases antigravity, antigravity-cli)
Thinnest detection of the three: no idle rule at all, so every agy idle is a guess
(invariant 2). Nothing in the pre-flight is recoverable once the agent is up.
D=/abs/dir/for/this/agent # ONE dir per agent, holding only its brief+input
mkdir -p "$D"; : > "$D/report.md" # pre-create the output file
python3 - "$D" <<'PY' # pre-trust; the dialog is unanswerable (below)
import json, pathlib, sys
p = pathlib.Path.home() / ".gemini/trustedFolders.json"
d = json.loads(p.read_text()) if p.exists() else {}
d[sys.argv[1]] = "TRUST_FOLDER" # "TRUST_PARENT" on a parent covers its children
p.write_text(json.dumps(d, indent=2) + "\n")
PY
P=$(herdr tab create --workspace "$WS" --cwd "$D" --label ag1 --no-focus \
| jq -r .result.root_pane.pane_id) # one TAB per agent, never N panes in one tab
herdr agent start ag1 --kind agy --pane "$P" --timeout 120000 \
-- --model gemini-3.7-flash-medium --effort medium --mode accept-edits
herdr agent read ag1 --source detection --lines 15 # read the banner, not the status
herdr agent prompt ag1 "Carry out $D/brief.md. Write your report to $D/report.md." \
--wait --timeout 1800000 & # then poll for the file, not the state
- Default to
gemini-3.7-flash-medium. Measured on par with Opus for rubric-driven bulk work (99.5% verdict agreement across 213 items, and it made the better call on the one they disputed) and far faster;gemini-3.8-flash-highscored measurably worse on the same task despite being the bigger, higher-effort model. Pick another id only when a task argues for it. - The trust dialog is unanswerable from this side.
agent startreturnsidle, the screen holdsDo you trust the contents of this project?, andsend-keys enterno-ops against it - repeatedly, silently, exit 0.--dangerously-skip-permissionsis blocked by the driving harness's classifier before it ever reaches herdr (invariant 5). Only the config entry works, and only if written beforeagent start. Trust is per exact path: a fresh subdirectory needs its own entry unless a parent carriesTRUST_PARENT. - One tab per agent. Four agy panes in one tab leaves ~29 columns: the TUI stops rendering, input races swallow prompts outright, and detection sees only the pane's own rows, so the state you read is wrong too.
- Read the startup banner before blaming herdr.
<account> (Google AI Pro)means entitlement is reaching Code Assist. A bare address, orEligibility check failed: UNAVAILABLE (code 503), means the agent is dead on arrival however healthy its status looks - re-auth, do not re-prompt. A silent drop to free tier also changes the data terms on everything you send it. not a valid artifact pathis agy-internal, not your filesystem.ArtifactMetadataattached to awrite_to_filereclassifies the write as an artifact, confined to~/.gemini/antigravity-cli/brain/<session-id>/. It self-recovers by dropping the metadata and retrying asEdit; pre-creating the file empty skips the round trip.- agy reads sibling files unprompted. Agents sharing a directory read each other's output
and a prior run's results, then reproduce them. Own directory each, and
md5any rerun before believing an agreement number - byte-identical multi-KB prose is copying, not consensus. - Premature
donelasts up to50s mid-turn while the pane visibly streams (grok shares this bug), so wait on the report file, never a settled state. A first prompt can be swallowed entirely with/.gemini/configagent_prompt_stalledand no trace in the composer; re-prompt, do notsend-keys. No session ref until the first prompt. Integration install target isantigravity-cli; config dir `must exist (orANTIGRAVITY_CLI_CONFIG_DIR`).
Integrations for all three are session-restore only - they never improve state detection - and
their hooks silently no-op without python3 on PATH. integration status cannot see a
codex-side disabled hook (WONTFIX); verify agent_session is present after the first codex
turn instead.
Launch
Shared mechanics; kind-specific flags and pre-flight are in Per kind.
test "${HERDR_ENV:-}" = 1 # never drive herdr from outside a pane
P=$(herdr pane split --current --direction right --cwd "$PWD" --no-focus | jq -r .result.pane.pane_id)
test -n "$P" # empty $P => misleading downstream errors
herdr pane process-info --pane "$P" # must be a bare shell at its prompt
For a fleet, give each agent its own tab instead (herdr tab create --workspace "$WS" --cwd "$D" --no-focus | jq -r .result.root_pane.pane_id): repeated splits leave each pane
narrow, and a TUI below its minimum width stops rendering, swallows input, and hides its own
state from detection.
Always agent read --source detection before the first prompt (invariant 2) and branch on
what it returns - never blind-fire a key at a dialog. After answering one:
herdr agent wait worker --until idle done --timeout 60000 # NOT a bare wait - see Drive
Rules: capture every ID from JSON, never predict. Names [a-z][a-z0-9_-]{0,31}, namespaced
(myproj-reviewer, never driver); names die with the agent - re-attach via
agent rename <pane> <name>. Fleets get their own workspace, --no-focus everywhere. After a
killed agent start, run herdr agent get <name> once to free the name reservation
(reconciliation is lazy - it can take a minute or two).
Shapes
The facts you need while composing a command, not after it fails.
Output modes. agent get, agent list, agent start, agent prompt, agent wait,
pane split, pane list, pane process-info, workspace * return JSON - pipe to jq.
agent read and pane read return raw pane text with no wrapper; piping them to jq
dies with parse error: Invalid numeric literal and you silently lose the read. Use tail,
grep -qF.
Positional vs flagged, and it differs per command. pane close <id> and
pane read <id> take the pane positionally - pane read has no --pane at all, so
herdr pane read --pane "$P" dies with unknown option: --pane. pane split accepts either
form. pane layout and pane process-info take --pane; pane list takes neither. When in
doubt read the Usage: line, which names positionals in angle brackets.
An agent TARGET can be a bare pane id, not just a name: agent get w1K:p1,
agent read w1K:p1, agent prompt w1K:p1 '<text>' all work. That is how you reach an agent
that was never named, without renaming someone else's. workspace create takes --label, not --name. send-keys takes key names only
(enter, esc, down, ctrl+c); text and slash commands go through prompt.
Flag dependencies and caps. agent start --timeout defaults to 30000 and is capped at
300000. agent prompt --timeout has no cap (1800000 is fine) but is rejected outright with
--timeout requires --wait when --wait is absent - and under a backgrounded call that
failure is invisible and the prompt is never written. Fire-and-forget sends take no flags.
Sparse JSON. agent list entries are partially populated: name is absent (not null)
for unnamed agents, interactive_ready is absent on many, and a pending start appears as
{"agent_status":"unknown","launch_pending":true} with no agent or agent_session key.
Null-guard every string op: select((.name // "") | startswith("myproj-")).
Env vars go in --env KEY=VALUE at pane/tab/workspace creation, never
pane run 'export ...' (that sets them in a subshell the agent never sees). Repeated flags
go inline - building them in a shell variable fails, because zsh does not word-split
unquoted expansions and herdr receives the whole string as one argument:
E="--env A=1 --env B=2"; herdr pane split ... $E # WRONG: unknown option: --env A=1 --env B=2
herdr pane split ... --env "A=1" --env "B=2" # right
Bad flags print usage (some of it on stdout), so a | jq pipeline turns it into a
misleading parse error. Check exit status before parsing.
Drive
# Blocking send: one call, generous timeout, backgrounded.
herdr agent prompt worker "$(cat brief.md)" --wait --timeout 1800000
# Fire-and-forget note to a working agent: no flags at all.
herdr agent prompt worker "$(cat note.md)"
# On timeout: usually false - confirm before acting, never blind-resend.
herdr agent get worker # working = still on the turn
# Mid-turn dialogs: wait for blocked, inspect, surface to the human, answer via send-keys.
herdr agent wait worker --until blocked --timeout 120000
herdr agent read worker --source detection --lines 40
# Exit: confirm positively - shell back in foreground. Never regex the prompt.
herdr pane process-info --pane "$P" && herdr pane close "$P"
- Wait on the artifact, not the state. End every non-trivial brief with "write your full
report to
<path>and reply with only the path", and poll for the file.idlearrives on interrupted turns, on fleet parents that dispatched children and ended their turn, and for the entire multi-minute life of a claude background MCP task. A watcher written as[ "$s" != "working" ] && echo stoppedfalse-alarms on every one of those. --untildefaults toidle|done|blocked; keep it there for turn waits, because narrowing it is how waits hang. One exception: right after answering a dialog, a barewaitreturns the still-blockedstate within milliseconds - a no-op that looks like the key never landed. Gate on--until idle donethere, or pollagent get. A wait endingagent_not_runningafter the agent exited or moved is the event, not an error.doneandidleboth mean ready for input;doneis an unseen completion, and focusing the tab silently rewrites it toidle. Each TUI client tracks this independently, so its badge can disagree with the CLI. Gate decisions onagent_status, neverinteractive_ready(which stays true while blocked).- Long text is safe through
agent prompt- it uses bracketed paste, and 8 KB arrives intact. The 1024-byte (macOS) tty truncation is real but applies topane send-textand keystrokes. Pass file paths anyway when the text is a brief: it also keeps the content out of the shell command, where the driving harness's classifier can block on it. - Output:
--source recent-unwrappedfor transcript,detectionfor the last screen,visiblefor what is on screen now. - Queue follow-ups behind a working agent freely;
agent promptrefuses blocked agents (agent_blocked) before writing anything. But do not attach--waitto a queued follow-up: it can be satisfied by the turn already running. - Slash commands that open dialogs trip the fixed 5s
agent_prompt_stalledgate - verify those by screen read, not--wait.
Failures
Triage first:
- Any start failure ->
herdr pane read <pane> --source visibleFIRST. The most common cause is a bad flag: the child launched, printederror: unknown option ..., and exited, leaving only the shell - soprocess-infolooks innocent and herdr reports a baretimeout. Thenpane process-info --pane <id>: a foreign foreground process = busy/race; an agent under a wrapper (node, a bare version string) = herdr cannot identify it -> relaunch viaHERDR_AGENT=<kind> exec <cmd>, thenagent rename. An agent underdocker exec,podman exec, orssh -tis permanently undetectable (WONTFIX). - Any wrong state ->
herdr agent explain <target> --verbose+agent read --source detection. Matched rule null +default_known_agent_idle_fallback= herdr is guessing. Stale manifest ->herdr server update-agent-manifests(no restart; named servers now pick up each other's downloads). Local rule patch:~/.config/herdr/agent-detection/<kind>.toml. - Logs:
~/.config/herdr/herdr-server.log,HERDR_LOG=herdr=debug. - After a binary update, do NOT reflexively
server stop. 0.9.0 leaves a compatible server and its running agents untouched; checkherdr status(endpoint_compatible,restart_needed,server_binary_stale) and stop only for server-side changes - stopping kills every pane process and in-flight turn, unresumably. A stale client inverts the advice: it reports the running server as "old" whilepane listcorrectly saysclient protocol N is older than server. Trustherdr status. - "herdr is stuck" is often the terminal emulator, not herdr. A wedged surface takes no input while panes and agents are fine in the server. Open a fresh tab before touching the server.
agent_not_ready (start) - a dialog is on screen. Exit 1 but the agent is running and the
name is bound (documented contract). Read, answer via send-keys, wait on --until idle done,
prompt. A blocked launch never times out: launch_pending stays true and rename returns
agent_launch_pending until the dialog is answered or the process exits. Both codex's
trust-directory dialog and its startup update dialog are detected, and claude's folder-trust
dialog is caught by the generic blocked-form rule - agy's trust dialog is not, and reads
as idle instead. Codex's one-time post-integration install hooks-review gate is NOT
detected (WONTFIX) - answer it once per machine.
timeout (start) - invariant 3; read the pane. command not found in a non-login shell:
set [terminal] shell_mode = "login", recreate the pane. Under --remote, panes inherit the
server's PATH, so a ~/.local/bin agent CLI is not found and you get a bare timeout.
agent_name_taken / agent_launch_pending - reservations are made before launch and
reconciled lazily: herdr agent get <name> once frees an expired one. Still wedged (rename ->
pending, get -> not_found, restart -> pane_busy): burn both - fresh pane, fresh name. Names are
cross-workspace and freed names get recycled - namespace them.
agent_pane_busy - "available shell" = the shell itself, alone, in the foreground. Three
cases: (a) racy - shell still running its rc files (starship, direnv); herdr retries only
2s (never on Windows) and pane get looks identical ready vs not, so retry with backoff and
clean up any orphaned tab; (b) genuine occupant - split a new pane, do not reclaim (killing the
occupant cascades into pane_not_found); (c) permanent on Windows - a profile that
chain-launches pwsh nests shells invisibly: [terminal] default_shell = "pwsh.exe".
agent_not_found - downstream symptom: failed start, exited agent, or a bad name earlier
in the loop. A pane holding only a shell also returns it. A live pane can rarely lose
registration while the TUI runs fine - agent rename <pane> <same-name> restores it.
timeout (prompt --wait / wait) - usually the turn outlasted the timeout: agent get
shows working. Use 1800000+, background it, never resend on timeout alone.
agent_prompt_stalled - the 5s gate is fixed and now starts after submission, accepting
only observed working or blocked; the caller timeout includes submission time, and expiring
first returns timeout instead. It is not proof of non-delivery - the text may have landed
and been consumed. Causes in observed order: stale manifest; dialog-opening prompt; a
target-side paste modal swallowing Enter (omp's Large Paste Menu, which triggers on line
count, not bytes - disable it in omp /settings); Windows input races on long prompts. Never
blind-resend and never recover with a lone send-keys enter (it can silently no-op). Read the
pane, then re-send with a fresh agent prompt - not a keystroke:
for i in 1 2 3; do
herdr agent prompt "$A" "$TEXT" --wait --timeout 60000 && break
herdr agent read "$A" --source recent-unwrapped --lines 200 | grep -qF "${TEXT:0:80}" && break
done
agent_blocked (prompt) - refused before anything is written. Read detection, surface the
dialog, answer via send-keys.
invalid_agent_name - grammar above; shell loops producing uppercase are the classic
cause, and one bad name cascades into a wall of agent_not_founds.
pane_not_found / workspace_not_found / unknown option: <valid-looking value> - IDs are
runtime-only, never reused: re-list at session start, recreate only what is missing.
unknown option on a whole flag string is the shell-variable trap (see Shapes); on a lone
token it is usually an empty $P making the parser blame the wrong thing.
workspace_group_close_required - closing a primary workspace with open worktree
workspaces needs workspace close --group. Closing the last tab closes its workspace. tab close can likewise return
confirmation_required when it would close a whole worktree group.
Harness blocks - classifier denial on dangerous passthrough flags: put permissiveness in
the child agent's own config ("Stage 2 classifier error" is transient - retry once). Allowlist
read-only commands (agent get/read/list/wait/explain, pane read/list/process-info,
workspace list) or every call prompts.
Silent failures (exit 0, no error): fallback-idle prompt swallowing (invariant 2); send-keys
returning ok without delivering; bare agent prompt leaving text unsubmitted in an
out-of-view pane; tty truncation of long send-text; a first turn going straight
unknown -> working -> idle skips done and its notification; detection sees only the pane's
own rows (fallback 24), so a tall dialog in a short pane is partly invisible;
pane wait-output matches the echoed command itself - never use it for readiness;
agent focus can return ok without moving the viewport in 0.9.0 - use tab focus instead.
Multiple machines (0.9.0)
herdr machine manages saved SSH endpoints, but it is a TUI-only surface: there is no
machine namespace in the CLI or socket API. IDs and live agent names are scoped to one server,
two machines can both hold w1:p1 or an agent named reviewer, and selecting a machine in the
TUI does not retarget CLI commands run in your pane - they still use the inherited socket.
For automation, run the herdr CLI on the intended host over ssh and rediscover IDs there. Only
add, remove, enable, or disable profiles when the user asks; setup asks before stopping an
incompatible remote server and defaults to No - do not approve replacement without consent.
--trust-repository on worktree grants per-request Git trust and is not a routine retry.