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

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

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

この記事でわかること

  • 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 の出力は末尾が空行で埋まる

最初にこう書きました。

Bash
tmux send-keys -t work 'python3' Enter
tmux capture-pane -t work -p | tail -10

何も返ってきません。コマンドが失敗したのかと思って調べたら、ペインは正常に動いていましたcapture-pane はペインの高さぶんの行を返すので、下のほうは空行で埋まります。tail を取ると、その空行だけを掴んでいたわけです。

空行を畳んでから末尾を取れば解決します。

Bash
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つのウィンドウを複数に分けるマルチプレクサ、その中で動くシェルやエージェント

A. 中身だけ入れ替える Ghostty ターミナルエミュレータ(そのまま) tmux ⇄ herdr マルチプレクサ(ここだけ交換) claude zsh SSH 先でも動く / Linux でも動く B. ターミナルごと乗り換える cmux libghostty を内蔵 上の2層を1つのアプリが持つ claude 埋め込みブラウザ macOS 専用 / SSH 先では使えない
tmux と herdr は同じ枠に収まるので入れ替えるだけで済む。cmux は上の2層を1つのアプリとして持つため、選んだ時点で Ghostty を手放すことになる。色を付けた枠が、選択によって置き換わる部分。

tmux と herdr は同じ枠の入れ替えで、cmux だけが1つ上の層にいます。cmux は Ghostty のレンダリングエンジン(libghostty)を内蔵した macOS ネイティブアプリなので、選んだ時点で今使っているターミナルを手放すことになります。

これが分かると選択がぐっと楽になります。「herdr と cmux のどちらか」で迷っているとき、実際に選んでいるのは ターミナルアプリごと乗り換えるかどうか です。SSH で作業する人にとっては、この時点で cmux は選択肢から外れます。

誰の席を最適化しているか

層の話で cmux は分離できましたが、tmux と herdr の差はまだ残ります。ここを一言で射抜いた表現がありました。

herdr はエージェントが座る席を最適化し、cmux は私が座る席を最適化している。

tmux はどちらの席も特別扱いしません。エージェントという概念が存在しなかった時代からある汎用ツールなので、当然といえば当然です。

人間が操作しやすい エージェントが操作しやすい cmux 縦タブ・通知・埋め込みブラウザ tmux どちらにも特化しない汎用ツール herdr 状態検知・待機 API・エージェント間の連携
同じ層にいても向いている先が違う。tmux が中央なのは劣っているからではなく、エージェントという概念を持たない汎用ツールだから。縦位置に意味はなく、ラベルの重なりを避けているだけ。

tmux が中央にいるのは劣っているからではありません。どちらにも寄っていないという設計です。だからこそ SSH 先でも Linux サーバーでも確実に動きます。

herdr を入れて、同じ検証を通す

実際に入れて確かめました。Homebrew に公式 formula があります。

Bash
brew install herdr
brew services start herdr   # 常駐サーバーとして起動
herdr integration install claude   # Claude Code 連携フックを入れる

integration install~/.claude/hooks/ にフックスクリプトを置き、settings.jsonSessionStart フックを追記します。既存の設定は壊れませんでしたが、心配なら先にバックアップを取ってから差分を見るとよいです。

先ほどと同じ検証を通した結果がこちらです。

検証ケース tmux + 自作ラッパー herdr
通常コマンドの完了待ち 0.46 秒(プロンプト復帰を検出) 同等(文字列マッチ)
REPL の起動待ち 4.1 秒(静止3秒ぶんの下駄あり) 3.1 秒(プロンプトが出た瞬間)
無音で5秒かかる処理 誤判定のリスクあり 5.11 秒で正確に復帰
読み取り結果 末尾が空行で埋まる そのまま使える
エージェントの状態 取得できない working / blocked / done / idle

決定的なのは3行目です。tmux 側の「画面が静止したら完了」という推測を、herdr は wait-output という文字列マッチの確定条件に置き換えます。

Bash
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秒で返ってきて気づきました。

1. pane run が送る文字列 sleep 5 && echo MARKER MARKER で待つ、と指定したつもり 2. ペインの画面に出るもの % sleep 5 && echo MARKER コマンド自体のエコー。送った瞬間に出る wait-output はここに当たり、t=0.08s で返る MARKER 本当の実行結果。t=5s に出る → 5秒待つつもりが、実行前に「完了」が返る
pane run はコマンド文字列をペインに打ち込むので、その文字列自体が画面に残る。待ち受ける語がコマンドに含まれていると、実行前のエコーに当たって即座に成功が返る。出力にしか現れない値を待つか、行頭・行末のアンカーを付けて回避する。

pane runコマンド文字列をペインに打ち込むので、その文字列自体が画面に残ります。wait-output はスナップショットを検索する仕様(既にある出力にもマッチする)なので、待ち受ける語がコマンドに含まれていると、実行前のエコーに当たってしまいます。

回避策は単純で、出力にしか現れない文字列で待つことです。

Bash
# ダメな例: 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 管理下のペインで動いていることを意味します。つまり、恩恵を受けるには起動手順が変わります。

Bash
# これまで
$ claude

# これから: herdr の中で起動する
$ herdr
  └─ (herdr のペイン内で) $ claude

CLI 自体は 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 を使っています。同じ層のツールなので、使い分けというより「どちらを主にするか」の話です。

参考リンク