メインコンテンツへスキップ
kt-tech.blog

【設定・環境構築】herdr を実際に使ってみる — 「エージェントの状態が見える」は何ができることなのか

設定・環境構築14分で読めます

この記事でわかること

  • herdr の中身は agent_status という1つの値で、pane → tab → workspace に集約されること
  • 「人の入力を待っている(blocked)」を herdr が自分で判定するので、画面の静止から推測しなくてよくなること
  • agent explain で「どのルールが何にマッチしたか」まで読めること
  • agent wait / agent prompt --wait で「終わるか、止まったら返す」を1行で書く方法
  • ペインが小さいと入力待ちを idle と誤判定する、という実地で踏んだ条件

herdr 0.8.0 / macOS / Claude Code を日常的に使っていること。前の2記事(どの層で戦っているか・どう積むか)を読んでいると繋がりますが、本記事単体でも読めます。

前の記事で Ghostty + herdr という構成に決めたんですが、正直に言うと、そのあとしばらく tmux と同じ使い方しかしていませんでした。 ペインを割って、Claude Code を動かして、たまに pane read で覗く。それだけなら tmux でよかったわけで。

herdr を入れる理由は「エージェントの状態が見える」ことのはずなんですが、その「見える」が具体的に何をしてくれるのかを分かっていなかったんですよね。なので今回は実際に手を動かして、そこだけを掘りました。

結論を先に言うと、herdr の中身は agent_status というたった1つの値でした。そしてこれが、tmux で自分が書いていた「画面が3秒止まったら入力待ちとみなす」というヒューリスティックを、丸ごと不要にしてくれます。

メモ

この記事でやること

  • agent_status の状態遷移を、実際に Claude Code を動かしながら観測する
  • herdr が「なぜその状態だと判断したか」を読む(agent explain
  • 状態で待つ(agent wait / agent prompt --wait)を tmux のやり方と比べる
  • 実際に踏んだ、検知が当てにならなくなる条件

手を動かす前の状態

環境は herdr 0.8.0、brew services で常駐させています。この記事の操作はすべて CLI(herdr <subcommand>)から叩いていて、画面をキーボードで操作した部分はありません。

まず気づいたのが、サーバーが動いていればワークスペースもペインも CLI だけで作れることでした。TUI を開く必要がない。

Bash
herdr workspace create --cwd ~/path/to/repo --label "herdr-demo" --no-focus

返ってくる JSON がこれで、いきなり本題が出てきます。

JSON
{
  "root_pane": { "pane_id": "w2:p1", "agent_status": "unknown", ... },
  "tab":       { "tab_id": "w2:t1", "agent_status": "unknown", ... },
  "workspace": { "workspace_id": "w2", "agent_status": "unknown", ... }
}

agent_statusペイン・タブ・ワークスペースの3階層すべてに付いている。この時点で「あ、これが本体か」と分かりました。

状態を実際に動かしてみる

ペインにエージェントを起動します。herdr agent start は起動するコマンドを知っていて、--kind で種類を指定します。

Bash
herdr agent start demo-claude --kind claude --pane w2:p1

--kind に渡せる値が21種類ありました。

pi, claude, codex, gemini, cursor, devin, agy, cline, omp, mastracode,
opencode, copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, maki

起動直後に状態を見ると、こうなっていました。

demo-claude   w2:p1   blocked

画面を覗くと、Claude Code の workspace trust ダイアログで止まっていました。こちらが何も教えていないのに、herdr が「人の入力を待っている」と判定している。 ここが tmux との決定的な差だと思っていて。tmux でこれをやろうとすると、画面が変化しなくなったことから推測するしかないんですよね。

Enter を送って、状態が変わるのを待ちます。

Bash
herdr agent send-keys demo-claude Enter
herdr agent wait demo-claude --until idle --timeout 60000

wait が返るまで 558ms でした。自分が書いていた tmux 用のヘルパーは「画面が3秒静止したら入力待ちとみなす」という判定なので、そもそも 3秒待たないと何も分からない。しかも「入力待ち」なのか「まだ考えている」のかは区別できません。

そのあとプロンプトを投げて、状態を1秒おきに追ってみました。

[01:41:53] idle
[01:41:56] working
[01:41:58] working
   …
[01:42:08] done

working の次が idle ではなく done なのが面白くて。ターンが終わって出力が出た直後だけ done になり、そのあと idle に落ちます。「まだ読んでいない結果がある」状態を分けて持っているんですね。

ペインごとに付く状態 unknown 起動 idle 投げる working done 読み終えたら idle に戻る 許可待ち・ダイアログ blocked 人が答えるまで進まない = 通知して呼ぶべき状態 上位へ集約される pane(1エージェント) blocked pane working tab 配下の状態をまとめる workspace サイドバーに出る herdr agent list / herdr agent wait は、この状態をそのまま返す herdr agent explain は「どのルールで・何を根拠に」その状態にしたかを返す
herdr が持っているのは、ペインごとの agent_status という1つの値。これがタブ・ワークスペースへと「一番強い状態」で集約されるので、サイドバーを見れば手を止めているエージェントが一目で分かる。tmux にはこの層が無いので、同じことをやるには画面の静止などから自分で推測することになる。

3つのエージェントを別々の状態にして並べると、サイドバーはこうなります。

herdr のサイドバー。3つのエージェントが working(琥珀の●)・idle(緑の○)・blocked(赤の●)で並んでいる
herdr のサイドバー。3つのエージェントが working(琥珀の●)・idle(緑の○)・blocked(赤の●)で並んでいる

上から working(琥珀の●)、idle(緑の○)、blocked(赤の●)です。赤を探せばいい、というのがこの機能の実用上の意味だと思っていて。5つ走らせていて1つが許可待ちで止まっている、というのが画面を読まずに分かる。

全体だとこう見えます。左が状態一覧、右が3つのペイン(作業中の Claude Code、終わった Claude Code、信頼ダイアログで止まっている Claude Code)。

herdr の全体画面。左サイドバーに3つのエージェントの状態、右に3つのペインが並んでいる
herdr の全体画面。左サイドバーに3つのエージェントの状態、右に3つのペインが並んでいる

「なぜその状態なのか」が読める

ここが一番良かったところです。herdr agent explain を叩くと、判定の根拠が出ます。

agent: claude
state: idle
manifest: remote:~/.local/state/herdr/agent-detection/remote/claude.toml 2026.08.21.1
rule: live_prompt_box (region=prompt_box_body priority=950)
evidence: "❯\n"

どのマニフェストの、どのルールが、何にマッチしたかまで返る。状態がおかしいときに「herdr がバグってる」で終わらせずに済みます。

マニフェストの実体は ~/.local/state/herdr/agent-detection/remote/ にあって、エージェント種別ごとの TOML が19個入っていました。claude.toml を覗くと、優先度付きのルールが16本。

TOML
[[rules]]
id = "osc_title_working"
state = "working"
priority = 1100
region = "osc_title"
regex = ['^[\x{2800}-\x{28FF}\x{25D0}-\x{25D3}] ']

最優先(priority 1100)が OSC タイトルに出るスピナー文字を見るルールでした。Claude Code は端末のタイトルに点字ブロック(⠋⠙⠹…)のスピナーを書いているので、画面本文を読まずにタイトルだけで「動いている」と判定できる。うまいなと思いました。

region の種類も面白くて、こう分かれています。

region 何を見るか
osc_title / osc_progress 端末のタイトルやプログレス(本文を読まない)
prompt_box_body 入力欄の中身
after_last_horizontal_rule 最後の区切り線から下(ダイアログが出る場所)
bottom_non_empty_lines(N) 末尾 N 行の空でない行
last_non_empty_above_prompt_box 入力欄の直上
whole_recent 直近の画面全体

blocked を判定するルールは5本あって、許可プロンプト・確認フォーム・動的ワークフローの確認などを個別に見ています。

TOML
[[rules]]
id = "bash_permission_prompt"
state = "blocked"
priority = 850
region = "whole_recent"
contains = ["do you want to proceed?"]
any = [
  { contains = ["bash command"] },
  { contains = ["tab to amend"] },
  { contains = ["ctrl+e to explain"] },
]

つまり herdr は汎用的に賢いのではなく、エージェントごとの画面の癖を知識として持っているわけです。claude.toml だけ更新日が新しかった(他が8月14日で claude が8月21日)のも納得で、Claude Code の UI が動くたびに追従が要るということでもあります。ここは裏を返すとメンテに追随できなくなった瞬間に壊れるので、後述の落とし穴につながります。

状態で待つ

実用上いちばん効くのがこれです。プロンプトを投げて、終わるまで待つ、が1行になります。

Bash
herdr agent prompt demo-claude "app.py に docstring を1行足して" --wait --until idle --until blocked

--until を複数書けるので、「終わったか、または許可待ちで止まったら返ってこい」が表現できます。実測で 13.4 秒後、idle で返ってきました。

これが tmux との差で、自分の tmux ヘルパーは結局こういう作りでした。

Bash
# 前面プロセスがシェルに戻ったか? → REPL や対話型エージェントでは戻らない
# 画面が IDLE_SEC 秒変化しないか?  → 「入力待ち」と「熟考中」を区別できない

推測をやめられるのが agent wait の価値だと思っていて。特に「許可待ちで止まった」を検知できるのが大きくて、投げっぱなしにして放置しても、止まったところで気づけます。

返ってきた JSON には Claude Code 側のセッション ID も入っていました。

JSON
"agent_session": { "agent": "claude", "kind": "id", "value": "da50f3b9-f03b-46a0-b6bb-bcafb59b5e9c" }

ペインと Claude Code のセッションが紐づいているので、claude --resume 側の情報と突き合わせられます。

踏んだ落とし穴

ここからが実際に触らないと分からなかった部分です。

画面が小さいと検知が落ちる

一番効いたのがこれでした。クライアントを1つも繋いでいない状態だと、herdr のセッションは 54×23 という小さい既定サイズで動きます。 この状態でモデル選択ピッカーを開くと、herdr は idle を返しました。

state: idle
rule: osc_title_idle (region=osc_title priority=250)

実際にはピッカーが開いていて入力待ちなのに、です。原因は単純で、ピッカーがペインより縦に長く、判定に必要な Esc to cancel などの行が画面外に押し出されていたから。優先度の高いルールがどれもマッチせず、優先度250の「タイトルが ✳ Claude Code だから idle」に落ちていました。

クライアントを繋いでペインが 48行になると、同じ画面でちゃんと別のルールを拾いました。

検知の根拠は「いま画面に出ている文字列」なので、ペインの大きさで結果が変わる。 agent wait で自動化を組むなら、ヘッドレスで小さいまま回さないほうがいいです。ここは正直、事前には想像していませんでした。

モデルピッカーは意図的に検知対象外

上のを大きい画面で試したら、今度はこうなりました。

state: unknown
rule: model_picker_menu (region=whole_recent priority=900)
skipped_update_reason: matched_rule:model_picker_menu

blocked ではなく unknown にして状態更新をスキップしています。モデルピッカーは「エージェントが人を待っている」のではなく「人が自分で開いた UI」なので、blocked 扱いしないという設計判断ですね。skipped_update_reason まで返してくれるので、これも explain で追えます。

agent wait は既定だと unknown を拾わない

--until を省略すると idle / done / blocked のいずれかで返ります。上のように unknown に落ちるケースがあるので、unknown で止まると wait が返ってこないことになります。--timeout は付けておいたほうが安全です。

worktree との組み合わせ

もう一つ実用的だったのがこれ。

Bash
herdr worktree create --cwd ~/path/to/repo --branch feature/greet-i18n

1コマンドで git worktree を切って、そこを開いた新しいワークスペースまで作ります。

$ git worktree list
/…/herdr-demo                                        f93248e [master]
/Users/…/.herdr/worktrees/herdr-demo/feature-greet-i18n  f93248e [feature/greet-i18n]

置き場所が ~/.herdr/worktrees/<repo>/<branch> で、リポジトリの外なのがポイントです。ちょうど前の記事で書いた Claude Code の Remote Control は .claude/worktrees/ とリポジトリ内に切るので、そこは逆になっています。.gitignore を気にしなくていい代わりに、git worktree list を見ないと存在に気づきにくい。

「1タスク1worktree」に別々のツールが独立に辿り着いているのは、それだけ並行作業で同じディレクトリを共有するのが事故りやすいということなんだろうなと。

で、結局どう使うのか

実際に触ってみて、自分の中ではこう整理できました。

効くのは、エージェントを2つ以上並行で回すとき。 1つしか動かしていないなら、その画面を見ていればいいので herdr の状態検知は要りません。3つ4つになった瞬間に「どれが止まっているか」を人間が巡回して確かめるのが辛くなって、そこで赤い●が効いてきます。

自動化を組むなら agent wait に寄せる。 「投げて、止まったら教えて」が1行で書けるので、pane wait-output で文字列を待つ書き方(前の記事で自己マッチの罠を書いたやつ)より事故りにくいです。文字列を待つのは、エージェント以外のコマンド(ビルドやテスト)に取っておくのがよさそう。

逆に、まだ使わなくていい場合もはっきりしました。SSH 先で作業するなら herdr は入っていないので tmux のままですし、エージェントが1つなら素の Ghostty で足ります。検知ルールがエージェントの UI 更新に追随する前提の仕組みなので、マイナーなエージェントを使っているなら期待しすぎないほうがいいとも思いました(19個のマニフェストのうち、更新が新しいのは claude.toml だけでした)。

まとめ

  • herdr の中身は agent_status という1つの値。unknown / idle / working / done / blocked の5つで、pane → tab → workspace に集約される
  • 「人の入力を待っている(blocked)」を自分で判定してくれるのが tmux との決定的な差。画面の静止から推測する必要がなくなる
  • 判定はエージェント種別ごとの TOML マニフェスト(優先度付きルール)で、agent explain で根拠まで読める。最優先は OSC タイトルのスピナー
  • ただし根拠は画面上の文字列なので、ペインが小さいと取りこぼす。ヘッドレスの既定 54×23 で回すと入力待ちを idle と誤判定した
  • agent wait / agent prompt --wait で「終わるか、止まったら返す」が1行。--timeout は付けておく
  • herdr worktree create は worktree とワークスペースを同時に作る。置き場はリポジトリ外の ~/.herdr/worktrees/

入れただけで放置していたのがもったいなかったな、というのが正直な感想です。特に explain があるおかげで、ブラックボックスに見えていた部分がほぼ全部読めるようになりました。

参考リンク