AI エージェント向けターミナル herdr を導入して、タブを使いやすくする
最近、ターミナルの常駐環境を tmux から herdr に乗り換えました。herdr は AI コーディングエージェント向けのターミナルワークスペースマネージャで、タブやペインごとに Claude Code を並走させて、各エージェントの working / idle をサイドバーで一覧できます。
導入自体はすんなり済むのですが、タブ周りは素のままだと2つ不満が出てきます。切り替えがプレフィックス方式(Ctrl+b → n)で毎回2段階なことと、タブ名が「1」「2」の連番で、どのタブに何をやらせているか切り替えてみないと分からないことです。
この記事では導入から始めて、この2つを順に解消します。設定とスクリプトはそのまま使える形で載せています。
herdr を導入する
herdr は Homebrew で入ります。herdr を実行すると常駐サーバー付きのセッションが立ち上がり、ターミナルを閉じても作業は残って、次に開いたときに再接続できます。この記事の時点のバージョンは 0.7.3 です。
brew install herdr
herdr
Claude Code と組み合わせるなら、公式連携も入れておきます。各ペインで動く Claude Code のセッション情報が herdr に報告されるようになるもので、後半のタブ名自動化はこの仕組みに助けられます。
herdr integration install claude

タブ切り替えを Shift+矢印にする
1つ目の不満、タブ切り替えから直します。herdr は tmux と同じプレフィックス方式で、デフォルトは Ctrl+b → n / p で隣のタブ、Ctrl+b → 1〜9 でジャンプです。毎回2段階の操作になるので、ブラウザのタブくらい気軽に行き来したいところです。
~/.config/herdr/config.toml に直接バインドを書くと、プレフィックスなしの一発切り替えになります。1つのアクションには配列で複数のキーを割り当てられます。
[keys]
previous_tab = ["shift+left", "shift+up"]
next_tab = ["shift+right", "shift+down"]
反映は herdr server reload-config だけで、再起動は要りません。最初は Ctrl+矢印 にするつもりでしたが、macOS では操作スペース(Spaces)の切り替えに取られていたのでやめました。
1つ注意として、直接バインドしたキーは herdr が横取りするので、ペイン内のアプリには届かなくなります。vim の Shift+←(単語移動)などを使う場合は、別のキーを選ぶのが良いと思います。
タブ名に Claude Code の作業内容を自動表示する
2つ目の不満はタブ名です。理想は「Claude Code にプロンプトを送るたびに、その内容がタブ名になる」ことですが、タブ名を自動で付ける設定は見当たりませんでした。連番か、手動リネーム(prefix+shift+t)だけです。
ただ、材料はそろっています。herdr のタブは CLI から操作できます。
$ herdr tab rename w1:t2 "請求書の集計"
一方 Claude Code には UserPromptSubmit フックがあり、ユーザーがプロンプトを送信するたびに、その内容を JSON で受け取れます。組み合わせれば「プロンプト受信時に自分のタブを rename する」が作れます。
フックを作る
~/.claude/settings.json にフックを登録します。
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash '/Users/me/.claude/hooks/herdr-tab-title.sh'",
"timeout": 5
}
]
}
]
}
スクリプト本体は次の通りです。そのまま ~/.claude/hooks/herdr-tab-title.sh に置けば動きます。
#!/bin/sh
# UserPromptSubmit フック: プロンプト先頭を herdr のタブ名に反映する
set -u
# フック入力(JSON)は stdin で来る。ヒアドキュメントと競合するので先にファイルへ退避
hook_input_file="$(mktemp)" || exit 0
trap 'rm -f "$hook_input_file"' EXIT
cat >"$hook_input_file" 2>/dev/null || true
[ "${HERDR_ENV:-}" = "1" ] || exit 0 # herdr のペイン内でだけ動かす
command -v herdr >/dev/null 2>&1 || exit 0
command -v python3 >/dev/null 2>&1 || exit 0
HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY' >/dev/null 2>&1
import json, os, subprocess, sys
with open(os.environ["HOOK_INPUT_FILE"], encoding="utf-8") as fh:
d = json.load(fh)
if d.get("agent_id"): # サブエージェントのイベントは無視
sys.exit(0)
prompt = " ".join(str(d.get("prompt") or "").split())
if not prompt:
sys.exit(0)
title = prompt[:19] + "…" if len(prompt) > 20 else prompt
session_id = str(d.get("session_id") or "")
out = subprocess.run(["herdr", "api", "snapshot"],
capture_output=True, text=True, timeout=3).stdout
snap = json.loads(out)["result"]["snapshot"]
agents = snap.get("agents") or []
# (1) セッションIDが herdr に登録済みなら、そのタブ
target = next((a["tab_id"] for a in agents
if (a.get("agent_session") or {}).get("value") == session_id), None)
# (2) フォーカス中のペインが「セッション未登録の claude」なら、そのタブ
if target is None:
fp = snap.get("focused_pane_id")
target = next((a["tab_id"] for a in agents
if a.get("pane_id") == fp and a.get("agent") == "claude"
and not (a.get("agent_session") or {}).get("value")), None)
# (3) どちらでもなければ何もしない
if target:
subprocess.run(["herdr", "tab", "rename", target, title],
capture_output=True, timeout=3)
PY
exit 0
タブの特定に環境変数の HERDR_TAB_ID を使わず、herdr api snapshot に問い合わせているのには理由があります。Claude Code のフックは共有デーモン配下で実行されることがあり、その場合、環境変数のタブ ID は実際のタブとずれるためです。スクリプトでは、セッション ID の照合で決まらなければ「いまフォーカスしているペインが Claude Code のとき」に限りそのタブを使い、どちらでもなければ何もしません。
2点だけ注意があります。UserPromptSubmit フックの stdout はモデルのコンテキストに注入されるため、スクリプトは何も出力しない作りにしています。また、フック構成はセッション起動時に読み込まれるので、すでに起動中のセッションには /hooks メニューを一度開くと反映されます。
これで、プロンプトを送るたびにその内容(先頭 20 文字)がタブ名になります。

所感
タブ名に作業内容が出るだけで、エージェントを並走させたときの見通しがだいぶ変わりました。「どのタブだっけ」とタブを行き来する時間が消えて、Shift+矢印の切り替えと合わせると体感はブラウザのタブに近づいています。
herdr は socket API と CLI が素直で、今回のような外付けの拡張が書きやすい作りでした。組み込みの自動タブ名がいずれ入る気もしますが、それまでこのフックで十分回りそうです。
Anycloudでは一緒に働くメンバーを募集しています!
Anycloudは、ユーザーの心を動かす体験を届けることを大切にしています。フルリモート・フルフレックスの環境のもと、ライフスタイルに合わせた働き方を実現しながら挑戦したい方を歓迎します。詳細はこちらをご覧ください。