# AI エージェント向けターミナル herdr を導入して、タブを使いやすくする

> AI エージェント向けターミナル herdr を導入し、Shift+矢印でのタブ切り替えと、Claude Code の作業内容をタブ名に自動表示するフックを整えました。設定とスクリプトはそのまま使える形で載せています。

- 公開日: 2026-07-08
- 著者: 村井 謙太
- タグ: Claude Code, ツール紹介
- URL: https://tech.anycloud.co.jp/articles/herdr-claude-code-tab-title

---

最近、ターミナルの常駐環境を tmux から [herdr](https://herdr.dev) に乗り換えました。herdr は AI コーディングエージェント向けのターミナルワークスペースマネージャで、タブやペインごとに Claude Code を並走させて、各エージェントの working / idle をサイドバーで一覧できます。

導入自体はすんなり済むのですが、タブ周りは素のままだと2つ不満が出てきます。切り替えがプレフィックス方式(`Ctrl+b` → `n`)で毎回2段階なことと、タブ名が「1」「2」の連番で、どのタブに何をやらせているか切り替えてみないと分からないことです。

この記事では導入から始めて、この2つを順に解消します。設定とスクリプトはそのまま使える形で載せています。

## herdr を導入する

herdr は Homebrew で入ります。`herdr` を実行すると常駐サーバー付きのセッションが立ち上がり、ターミナルを閉じても作業は残って、次に開いたときに再接続できます。この記事の時点のバージョンは 0.7.3 です。

```sh
brew install herdr
herdr
```

Claude Code と組み合わせるなら、公式連携も入れておきます。各ペインで動く Claude Code のセッション情報が herdr に報告されるようになるもので、後半のタブ名自動化はこの仕組みに助けられます。

```sh
herdr integration install claude
```

![herdr の画面。上部にタブ、左のサイドバーに各エージェントの working / idle 状態が一覧で並ぶ](./herdr-ui.webp)

## タブ切り替えを Shift+矢印にする

1つ目の不満、タブ切り替えから直します。herdr は tmux と同じプレフィックス方式で、デフォルトは `Ctrl+b` → `n` / `p` で隣のタブ、`Ctrl+b` → `1`〜`9` でジャンプです。毎回2段階の操作になるので、ブラウザのタブくらい気軽に行き来したいところです。

`~/.config/herdr/config.toml` に直接バインドを書くと、プレフィックスなしの一発切り替えになります。1つのアクションには配列で複数のキーを割り当てられます。

```toml
[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 から操作できます。

```sh
$ herdr tab rename w1:t2 "請求書の集計"
```

一方 Claude Code には [`UserPromptSubmit` フック](https://code.claude.com/docs/en/hooks)があり、ユーザーがプロンプトを送信するたびに、その内容を JSON で受け取れます。組み合わせれば「プロンプト受信時に自分のタブを rename する」が作れます。

## フックを作る

`~/.claude/settings.json` にフックを登録します。

```json
"hooks": {
  "UserPromptSubmit": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "bash '/Users/me/.claude/hooks/herdr-tab-title.sh'",
          "timeout": 5
        }
      ]
    }
  ]
}
```

スクリプト本体は次の通りです。そのまま `~/.claude/hooks/herdr-tab-title.sh` に置けば動きます。

```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 文字)がタブ名になります。

![タブ名に各タブの作業内容がそのまま表示されているタブバー](./herdr-tabs.webp)

## 所感

タブ名に作業内容が出るだけで、エージェントを並走させたときの見通しがだいぶ変わりました。「どのタブだっけ」とタブを行き来する時間が消えて、Shift+矢印の切り替えと合わせると体感はブラウザのタブに近づいています。

herdr は socket API と CLI が素直で、今回のような外付けの拡張が書きやすい作りでした。組み込みの自動タブ名がいずれ入る気もしますが、それまでこのフックで十分回りそうです。
