herdr-faq

Launch and drive coding agents (codex, claude, agy) through the Herdr CLI; requires a herdr pane (HERDR_ENV=1). Use before starting or prompting a herdr subagent, and when a herdr command fails or an

SKILL.md

GitHub

Herdr FAQ

Launch and drive coding agents through Herdr (>= 0.9.2) without losing prompts.

Syntax authority

herdr --skill (~14k chars) is the vendor reference: command model, IDs, lifecycle states, prompt and wait mechanics, read sources, safety rules. Read it once per session before your first herdr command - this file assumes it and repeats none of it. For flags, run the command group bare (herdr agent, herdr pane) or --help; never bare herdr (it launches the TUI). If a syntax question is not answered here, it is answered there - go get it, never guess a flag.

When this lane

test "${HERDR_ENV:-}" = 1     # never drive herdr from outside a pane

Unset means this shell is not herdr-managed: no $HERDR_PANE_ID, no --current, and an untargeted command lands on whatever pane the USER has focused. Use acpx instead (acpx-faq). In-session fan-out stays on the harness's own subagent tool.

Invariants

  1. Exit 0 is submission, never completion - and send-keys can return {"type":"ok"} for keystrokes that never reach the pane. Confirm by effect: state moved, or the text is visible in the pane.
  2. Screens no detection rule matches read as idle - codex's read as unknown instead. agy has no idle rule at all, so every agy idle is a guess. Read the screen before the first prompt, always.
  3. agent start timeout = the child never launched (bad flag, PATH, or a wrapper process), or it launched into an undetected gate. pane read has the real error; pane process-info and herdr's own message do not. The timeout frees the name but leaves the process running - close the pane before reusing it.
  4. idle is not "finished". A claude turn that spawned background shells or a background MCP task ends and reports idle while that work runs on; --wait tracks lifecycle state, not turns.
  5. 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 backgrounded prompt, or wait on a file.

Per kind

Model defaults unless the task argues otherwise: codex gpt-5.6-sol at reasoning_effort high (set via codex's own flags/config - check codex --help), claude claude-opus-5-5, agy gemini-3.7-flash-medium (measured on par with Opus for rubric-driven bulk work and far faster; gemini-3.8-flash-high measured worse on the same task). One directory per agent, always.

codex

D=$(cd "$D" && pwd -P)       # codex's sandbox rejects a writable root with a symlink component
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 wedge the pane in startup-pending. The trust-directory and startup-update dialogs are detected, so an untrusted dir fails fast with agent_not_ready rather than eating the first prompt. 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; an explicit -- resume <id> is persisted at launch. A plain title and composer cannot prove a turn ended, so codex can sit at unknown after answering and --wait then runs to its timeout - wait on the report file.

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 dir under an already-trusted parent needs nothing.
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 claude-opus-5-5 --permission-mode auto   # acceptEdits blocks on routine approvals
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 &
  • The safe key differs per dialog. The folder-trust dialog puts the cursor on No, exit, so a bare enter kills the agent - send down enter. The Bash-permission dialog puts the cursor on 1. Yes.
  • idle lies in a specific way here. Background shells end the turn and report idle while they run; background agents and MCP tasks keep it working, and a background MCP task can hold idle for its whole 3-10 minute life.
  • Grey next-prompt-suggestion text reads as real unsubmitted text in a plain read - disambiguate with --format ansi (the suggestion carries the dim attribute ESC[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 via HERDR_AGENT=claude exec <path>, then agent rename.
  • Under a different CLAUDE_CONFIG_DIR the agent loads its own skills. Confirm they are current before its first turn - a running session has already snapshotted the old text.

agy (aliases antigravity, antigravity-cli)

Thinnest detection of the three: no idle rule at all (invariant 2), and nothing in the pre-flight is recoverable once the agent is up.

D=/abs/dir/for/this/agent                   # ONE dir per agent - agy reads sibling files
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/antigravity-cli/settings.json"
d = json.loads(p.read_text()) if p.exists() else {}
tw = d.setdefault("trustedWorkspaces", [])  # a list of exact paths, not a map
if sys.argv[1] not in tw: tw.append(sys.argv[1])
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
  • The trust dialog is unanswerable from this side. agent start returns idle, the screen holds Do you trust the contents of this project?, and send-keys enter no-ops against it - repeatedly, silently, exit 0. --dangerously-skip-permissions is classifier-blocked before it reaches herdr (invariant 5). Only a config entry written before agent start pre-empts it.
  • Trust lives in trustedWorkspaces in ~/.gemini/antigravity-cli/settings.json, NOT ~/.gemini/trustedFolders.json (the Gemini CLI's file - observed ignored on agy 1.2.0). Pre-writing it is not confirmed end to end, and a harness classifier may refuse the edit as a trust-config change - when it does, drive agy through acpx instead (acpx-faq): its ACP server has no folder-trust dialog.
  • One tab per agent. Four agy panes in one tab leaves ~29 columns: the TUI stops rendering, input races swallow prompts, 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, or Eligibility check failed: UNAVAILABLE (code 503), means dead on arrival however healthy the status looks - re-auth, do not re-prompt.
  • not a valid artifact path is agy-internal, not your filesystem. ArtifactMetadata on a write_to_file confines the write to ~/.gemini/antigravity-cli/brain/<session-id>/. It self-recovers by retrying as Edit; pre-creating the file empty skips the round trip.
  • agy reads sibling files unprompted - agents sharing a directory reproduce each other's output. Own directory each, and md5 any rerun before believing an agreement number: byte-identical multi-KB prose is copying, not consensus.
  • Premature done lasts up to 50s mid-turn while the pane visibly streams - wait on the report file, never a settled state. Command-permission prompts are undetected too and read as idle/done - --mode accept-edits avoids most. A first prompt can be swallowed entirely with agent_prompt_stalled and no trace in the composer; re-prompt, never send-keys. No session ref until the first prompt. Integration install target is antigravity-cli; `/.gemini/configmust 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 and fleets

  • Fleets: one tab per agent (herdr tab create --workspace "$WS" --cwd "$D" --no-focus), own workspace, --no-focus everywhere. Repeated splits leave panes narrow, and a TUI below its minimum width stops rendering, swallows input, and hides its state from detection (which sees only the pane's own rows; fallback 24).
  • Always agent read --source detection before the first prompt (invariant 2) and branch on it - never blind-fire a key at a dialog. After answering one, gate on agent wait <n> --until idle --until done --timeout 60000 (one state per --until; --until idle done fails with unknown option: done): a bare wait returns the still-blocked state within milliseconds, a no-op that looks like the key never landed.
  • Names die with the agent and freed names get recycled - namespace them (myproj-reviewer). After a killed agent start, one herdr agent get <name> frees the reservation (reconciliation is lazy).
  • Test -n "$P" after capturing a pane id: an empty one makes later parsers blame the wrong token.

Composing traps

  • agent read and pane read return raw pane text; the other agent/pane/workspace commands return JSON. Piping raw text to jq dies with parse error: Invalid numeric literal and you silently lose the read - use tail, grep -qF.
  • pane read <id> takes the pane positionally and has no --pane at all (unknown option: --pane); pane close likewise. When in doubt read the Usage: line - it names positionals in angle brackets.
  • agent start --timeout defaults to 30000 and must be >3000 and <=300000. agent prompt --timeout has no cap (1800000 is fine) but is rejected with --timeout requires --wait when --wait is absent - and backgrounded, that failure is invisible and the prompt is never written. Fire-and-forget sends take no flags.
  • agent list JSON is sparse: name is absent (not null) for unnamed agents, and a pending start is {"agent_status":"unknown","launch_pending":true}. 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 ...' (a subshell the agent never sees). Repeated flags go inline - zsh does not word-split an unquoted $E, so E="--env A=1 --env B=2" arrives as one bogus argument.
  • Bad flags print usage partly on stdout, so a | jq pipeline turns it into a misleading parse error - check exit status before parsing.

Drive

  • 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. idle arrives on interrupted turns, on fleet parents that dispatched children, and for the whole life of a claude background MCP task (invariant 4) - a watcher on != working false-alarms on every one of those.
  • Gate decisions on agent_status, never interactive_ready (it stays true while blocked). To count finished turns, compare completion_seq across reads - it marks completed work regardless of whether anyone has seen it, unlike done vs idle.
  • Long text is safe through agent prompt (bracketed paste; 8 KB arrives intact); the 1024-byte macOS tty truncation applies to pane send-text and keystrokes. Pass file paths anyway - it also keeps brief content out of the shell command (invariant 5).
  • Queue follow-ups behind a working agent freely (prompt refuses blocked agents before writing anything) - but no --wait on a queued follow-up: it can be satisfied by the turn already running.
  • Slash commands that open dialogs trip the fixed 5s stall gate - verify those by screen read, not --wait.
  • Exit: confirm positively - pane process-info shows the shell back in the foreground - then pane close. Never regex the prompt.

Failures

Triage first:

  • Any start failure -> herdr pane read <pane> --source visible FIRST. Most common: the child launched, printed error: unknown option ..., and exited - process-info looks innocent and herdr reports a bare timeout. Then process-info: a foreign foreground process = busy/race; an agent under a wrapper (node, a bare version string) = relaunch via HERDR_AGENT=<kind> exec <cmd>, then agent rename; under docker exec, podman exec, or ssh -t = undetectable unless the host-side wrapper itself is prefixed (HERDR_AGENT=codex docker exec -it ...) - set inside the container, herdr never sees it.
  • Any wrong state -> 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). Local rule patch: ~/.config/herdr/agent-detection/<kind>.toml, applied by herdr server reload-agent-manifests; herdr server agent-manifests shows which sources are active. Logs: ~/.config/herdr/herdr-server.log, HERDR_LOG=herdr=debug.
  • After a binary update, do NOT reflexively server stop - it kills every pane process and in-flight turn, unresumably. An update leaves a compatible server and its agents untouched; trust herdr status (endpoint_compatible, restart_needed, server_binary_stale). A stale CLIENT inverts the picture: it calls the server "old" while pane list correctly says client protocol N is older than server.
  • "herdr is stuck" is often the terminal emulator, not herdr - open a fresh tab before touching the server.

Catalog:

  • agent_not_ready (start) - a dialog is on screen; exit 1 but the agent is running and the name is bound. Read, answer via send-keys (safe keys in Per kind), gate --until idle --until done, prompt. A blocked launch never times out (launch_pending stays true until answered). agy's trust dialog is NOT detected and reads as idle instead - see agy.

  • 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 gives a bare timeout.

  • agent_name_taken / agent_launch_pending - reservations reconcile lazily: one herdr agent get <name> frees an expired one. Still wedged: burn both - fresh pane, fresh name.

  • agent_pane_busy - "available shell" = the shell itself, alone, in the foreground. (a) racy rc files (starship, direnv): herdr retries only 2s and pane get looks identical ready vs not - retry with backoff, clean up orphaned tabs; (b) genuine occupant: split a new pane, never reclaim (killing the occupant cascades into pane_not_found). (Windows: pwsh chain-launch profiles nest shells - set [terminal] default_shell = "pwsh.exe".)

  • agent_not_found - downstream symptom: failed start, exited agent, or a bad name earlier in the loop. 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 - not proof of non-delivery; the text may have landed and been consumed. Causes in observed order: stale manifest; a dialog-opening prompt; the user acting in the pane (e.g. switching model) as the prompt lands; a target-side paste modal swallowing Enter (omp's Large Paste Menu triggers on line count, not bytes - disable it in omp /settings). Never blind-resend and never recover with a lone send-keys enter. Read the pane, then re-send with a fresh agent prompt:

    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.

  • agent_not_idle (read) - agent read --lines N needs alternate-screen history, which is only captured while idle; a working/blocked/unknown claude refuses it. Use --source visible, or wait for idle and retry.

  • invalid_agent_name - uppercase from shell loops 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 zsh variable trap (see Composing traps); on a lone token it is usually an empty $P.

  • workspace_group_close_required - closing a primary workspace with open worktree workspaces needs workspace close --group; tab close can likewise return confirmation_required for a worktree group or a workspace's last tab.

  • 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 the 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; a bare agent prompt leaving text unsubmitted in an out-of-view pane; a tall dialog partly invisible in a short pane (detection sees only the pane's own rows); pane wait-output matching the echoed command itself - never use it for readiness.