【環境構築】Claude Code に対話コマンドを任せる — tmux・herdr・cmux はどの層で戦っているか

この記事でわかること
- tmux / herdr / cmux は競合ではなく、座っている層が違うということ
- Claude Code に対話型コマンド(REPL・ssh・認可コード待ち)を代行させる具体的な方法
- 同じ検証を tmux と herdr の両方に通した実測値と、tmux 側で踏んだ2つの罠
- herdr の wait-output がコマンド自身のエコーに自己マッチして、実行前に「完了」を返す罠と回避策
Claude Code を日常的に使っていること。tmux の基本操作(セッション・ペイン・デタッチ)を知っていると読みやすいですが、知らなくても読めるように書いています。
Claude Code に作業を任せていると、どうしても自分で打たなければいけないコマンドが出てきます。gcloud auth login のような対話的なログイン、python3 の REPL、OAuth の認可コードの貼り付け。Claude Code の Bash ツールは対話的なプロンプトを扱えないので、これらは「すみません、ここはご自身で打ってください」とお願いすることになります。
これを何とかしたくて調べたところ、tmux を経由すれば解決すること、そして最近話題の herdr と cmux がまさにこの領域のツールだということが分かりました。ただ、この3つの関係が最初はまったく整理できませんでした。同じものの競合に見えて、実は座っている層が違います。
この記事でやること
- tmux / herdr / cmux が「どの層のツールなのか」を確定させる
- Claude Code に対話型コマンドを代行させる方法を、tmux と herdr の両方で作る
- 同じ検証を両方に通して、実測値で比べる
- 両方で踏んだ罠を記録する(どちらにもあります)
そもそも何を解決したいのか
Claude Code の Bash ツールは、コマンドを投げて結果を受け取る形で動きます。この形だと、入力待ちで止まるコマンドが扱えません。パスワードを聞かれても答えられませんし、REPL に入ったら出てこられません。
ところが tmux を挟むと話が変わります。tmux は「ペイン」という単位で端末を持っていて、外から次の2つができます。
tmux send-keys— ペインにキー入力を送り込むtmux capture-pane— ペインの画面を文字列として読み取る
つまり エージェントが「画面を見て、キーを打つ」ことができるわけです。これができれば、対話型コマンドも代行できます。
tmux でやってみて踏んだ2つの罠
実際に組んでみると、素直には動きませんでした。
罠1: capture-pane の出力は末尾が空行で埋まる
最初にこう書きました。
tmux send-keys -t work 'python3' Enter
tmux capture-pane -t work -p | tail -10何も返ってきません。コマンドが失敗したのかと思って調べたら、ペインは正常に動いていました。capture-pane はペインの高さぶんの行を返すので、下のほうは空行で埋まります。tail を取ると、その空行だけを掴んでいたわけです。
空行を畳んでから末尾を取れば解決します。
tmux capture-pane -t work -p | grep -v '^[[:space:]]*$' | tail -10罠2: 「シェルに戻ったか」では完了を判定できない
もっと厄介なのがこちらです。コマンドの完了を待つために、「ペインの前面プロセスがシェルに戻ったら完了」という判定を書きました。tmux display-message -p '#{pane_current_command}' が zsh を返せば完了、という考え方です。
通常のコマンドならこれで動きます。ところが python3 のような対話型プロセスは起動したらシェルに戻ってきません。結果、REPL を立ち上げるだけでタイムアウトの120秒を丸ごと待たされました。
仕方がないので「画面が3秒間変化しなくなったら止まったとみなす」という判定を足しました。これで4.1秒まで縮みましたが、よく考えるとこれは推測です。無音のまま長く動く処理があれば、完了していないのに完了と誤認します。
この「いつ読めばいいか分からない」という問題が、後で効いてきます。
3つのツールは同じ土俵にいない
ここで herdr と cmux を調べたのですが、比較記事を読んでも噛み合いませんでした。原因は、3つが同じ層のツールだと思い込んでいたことです。
ターミナルまわりは層になっています。ウィンドウを描くターミナルエミュレータ、1つのウィンドウを複数に分けるマルチプレクサ、その中で動くシェルやエージェント。
tmux と herdr は同じ枠の入れ替えで、cmux だけが1つ上の層にいます。cmux は Ghostty のレンダリングエンジン(libghostty)を内蔵した macOS ネイティブアプリなので、選んだ時点で今使っているターミナルを手放すことになります。
これが分かると選択がぐっと楽になります。「herdr と cmux のどちらか」で迷っているとき、実際に選んでいるのは ターミナルアプリごと乗り換えるかどうか です。SSH で作業する人にとっては、この時点で cmux は選択肢から外れます。
誰の席を最適化しているか
層の話で cmux は分離できましたが、tmux と herdr の差はまだ残ります。ここを一言で射抜いた表現がありました。
herdr はエージェントが座る席を最適化し、cmux は私が座る席を最適化している。
tmux はどちらの席も特別扱いしません。エージェントという概念が存在しなかった時代からある汎用ツールなので、当然といえば当然です。
tmux が中央にいるのは劣っているからではありません。どちらにも寄っていないという設計です。だからこそ SSH 先でも Linux サーバーでも確実に動きます。
herdr を入れて、同じ検証を通す
実際に入れて確かめました。Homebrew に公式 formula があります。
brew install herdr
brew services start herdr # 常駐サーバーとして起動
herdr integration install claude # Claude Code 連携フックを入れるintegration install は ~/.claude/hooks/ にフックスクリプトを置き、settings.json に SessionStart フックを追記します。既存の設定は壊れませんでしたが、心配なら先にバックアップを取ってから差分を見るとよいです。
先ほどと同じ検証を通した結果がこちらです。
| 検証ケース | tmux + 自作ラッパー | herdr |
|---|---|---|
| 通常コマンドの完了待ち | 0.46 秒(プロンプト復帰を検出) | 同等(文字列マッチ) |
| REPL の起動待ち | 4.1 秒(静止3秒ぶんの下駄あり) | 3.1 秒(プロンプトが出た瞬間) |
| 無音で5秒かかる処理 | 誤判定のリスクあり | 5.11 秒で正確に復帰 |
| 読み取り結果 | 末尾が空行で埋まる | そのまま使える |
| エージェントの状態 | 取得できない | working / blocked / done / idle |
決定的なのは3行目です。tmux 側の「画面が静止したら完了」という推測を、herdr は wait-output という文字列マッチの確定条件に置き換えます。
herdr pane run w1:p1 'npm run dev'
herdr pane wait-output w1:p1 --match "ready on port 3000" --timeout 30000
herdr pane read w1:p1 --source recent-unwrapped --lines 30「出るべきものを名指しして待つ」ので、無音の時間がどれだけ続いても取りこぼしません。tmux 側で苦労した部分を、構造的に解いています。
さらに herdr agent prompt を使うと、エージェントが別のエージェントにプロンプトを投げて、状態が落ち着くまで待つこともできます。ここが「エージェントの席を最適化している」と言われる所以です。
herdr にも罠がある
いいことばかり書きましたが、検証中にしっかり引っかかりました。wait-output は、待ち受ける文字列の選び方を間違えると一瞬で嘘の完了を返します。
5秒かかる処理を待たせたつもりが、0.08秒で返ってきて気づきました。
pane run はコマンド文字列をペインに打ち込むので、その文字列自体が画面に残ります。wait-output はスナップショットを検索する仕様(既にある出力にもマッチする)なので、待ち受ける語がコマンドに含まれていると、実行前のエコーに当たってしまいます。
回避策は単純で、出力にしか現れない文字列で待つことです。
# ダメな例: MARKER はコマンド自身に含まれる
herdr pane run w1:p1 'sleep 5 && echo MARKER'
herdr pane wait-output w1:p1 --match "MARKER" # → 0.08秒で返る
# よい例: 42 はコマンド文には現れず、結果にだけ出る
herdr pane run w1:p1 'sleep 5 && echo $((40+2))'
herdr pane wait-output w1:p1 --regex "^42$" # → 5.11秒、正しく待つ行頭・行末のアンカー(^ と $)を付けるだけでも、かなり事故が減ります。
導入前に知っておくべきこと
herdr の agent skill には HERDR_ENV=1 という条件が書かれています。これは エージェント自身が herdr 管理下のペインで動いていることを意味します。つまり、恩恵を受けるには起動手順が変わります。
# これまで
$ claude
# これから: herdr の中で起動する
$ herdr
└─ (herdr のペイン内で) $ claudeCLI 自体は herdr の外からでも動きますが、skill は「外から操作するな」と明示しています。ユーザーがフォーカスしているセッションをエージェントが勝手に触らないための、まっとうな設計です。
もう1つ。send-keys でパスワードや API キーを送るのは避けたほうがいいです。会話ログに残ります。その場面になったら手を止めて、本人にペインへアタッチして入力してもらい、その後を読み取って再開するのが安全です。
結局どれを選ぶか
層と席、2本の軸で切ると素直に分かれます。
| ツール | 向いている場面 | 注意点 |
|---|---|---|
| tmux | SSH 先や Linux サーバー。枯れていて確実 | エージェント機能は自分で書くことになる |
| herdr | ターミナルは今のまま、複数エージェントの状態監視と待機APIが欲しい | まだ v0.8 系。破壊的変更の可能性はある |
| cmux | macOS で自分がエージェントを捌く体験を良くしたい | ターミナルアプリごと乗り換え。SSH 先では使えない |
私は herdr を選びました。決め手は wait-output です。tmux で自作した「画面が静止したら完了」という推測を捨てられるのが大きく、Ghostty をそのまま使えるので移行コストもほぼゼロでした。
ただし tmux の知識が無駄になったかというと、そうでもありません。herdr は tmux 風のキーバインドを踏襲していますし、SSH 先の作業では今も tmux を使っています。同じ層のツールなので、使い分けというより「どちらを主にするか」の話です。


