【tmux】Claude Codeのセッションの状態をサイドバーに一覧表示するプラグインを作った
tmux で Claude Code を複数起動していると、どれが承認待ちになっているのか分からなくなります。tmux の各ウィンドウに Claude Code の状態を一覧表示するプラグイン claude-tmux-sidebar の使い方、類似ツールがある中で自作した理由、Claude Code の hooks と Rust による実装を紹介します。
はじめに
こんにちは、とりかつ(@torikatsu923)です。
tmux の複数のウィンドウやセッションで Claude Code を起動していると、どれが稼働中で、どれが承認待ちになっているのかが分からなくなります。承認待ちの Claude Code を探すには、ウィンドウを順に開いて確認するしかありません。
そこで、Claude Code の状態を tmux のサイドバーに一覧表示するプラグイン claude-tmux-sidebar を作りました。この記事では、このプラグインの使い方、類似ツールがある中で自作した理由、Claude Code の hooks と Rust を使用した実装を紹介します。
torikatsupg/claude-tmux-sidebar - GitHub
claude-tmux-sidebar とは
claude-tmux-sidebar は、tmux の各ウィンドウの左端にサイドバーのペインを追加し、すべての tmux セッションで起動している Claude Code の状態を一覧表示します。どのセッションにいても常にサイドバーが表示されるので、ウィンドウを切り替えなくても、承認待ちや完了の Claude Code をすぐ把握できます。一覧のセッションを選択すると、その Claude Code のペインへ移動できます。
サイドバーには Claude Code のセッションが tmux のセッションごとにグルーピングして表示されます。各セッションの名前は ウィンドウ番号:ディレクトリ名 で、先頭のアイコンが状態を、右端の時間がその状態になってからの経過時間を示します。
- ◆: 承認待ち。ツールの実行の承認や、Claude からの質問への回答を待っている
- ✓: 完了
- ⠋ ⠙ ⠹ ⠸ … と切り替わるスピナー: 稼働中
- ○: 待機
稼働中と承認待ちのセッションには、ツールの名前(Bash、Edit など)も表示します。
既存の類似ツール
cmuxは Ghostty をベースにした macOS 用のターミナルです。縦タブのサイドバーがあり、エージェントが入力を待っているペインを青いリングで知らせます。
herdrは Rust 製のターミナルマルチプレクサで、tmux の代わりに使用するものです。各ペインのエージェントの状態を working・blocked・idle で表示します。
tmux のプラグインにも、エージェントの状態を表示するものがあります。claude-tmux-sidebar にいちばん近いのは tmux-agent-sidebar です。
| ツール | 対象のエージェント | 形態 | 機能の範囲 |
|---|---|---|---|
| hiroppy/tmux-agent-sidebar | Claude Code・Codex・OpenCode | tmux のサイドバー | 全セッションの状態の一覧に加え、プロンプトやツール呼び出しの内容、git の状態、worktree の作成と削除、デスクトップ通知 |
| Ataraxy-Labs/opensessions | Amp・Claude Code・Codex・OpenCode | tmux のサイドバー | 状態の一覧。スクリプトから HTTP で状態を送信できる |
| craftzdog/tmux-claude-hatch | Claude Code | tmux の popup | プロジェクトごとの Claude Code を popup で開き、fzf で選択する |
| raine/workmux | Claude Code・Codex・OpenCode・Gemini CLI など | git worktree と tmux のウィンドウを対にした開発ツール | worktree を中心にした並列開発。エージェントの状態表示はその一部 |
類似ツールがある中で自作した理由
cmux を使用するにはターミナルを、herdr を使用するには tmux を乗り換える必要があります。筆者は Alacritty と tmux で開発しており、ウィンドウ・セッション・ペインの管理は tmux で完結させたいと考えていました。
tmux のプラグインであれば、ターミナルや tmux を乗り換えずに使用できます。ただ、筆者は機能がシンプルなものを使いたかったので、既存のプラグインがある中であえて自作しました。claude-tmux-sidebar の機能は、Claude Code の状態の一覧とペインへの移動に絞っています。
インストールと使用方法
tmux と Claude Code のプラグインのインストール
動作には、次の環境が必要です。
- tmux 3.2 以降
- Claude Code
- macOS か Linux(arm64 か x86_64)
-
tmux のプラグインのインストール
TPMを使用している場合は、
~/.tmux.confに次の行を追加し、prefix + Iでインストールしてください。# v0.4.0 のタグに固定 set -g @plugin 'torikatsupg/claude-tmux-sidebar#v0.4.0' -
Claude Code のプラグインのインストールとセッションの再起動
Claude Code で次のコマンドを実行し、Claude Code のセッションを再起動してください。
/plugin marketplace add torikatsupg/claude-tmux-sidebar#v0.4.0 /plugin install tmux-sidebar@torikatsupg
詳細な手順はREADMEを参照してください。
サイドバーの開閉と Claude Code のペインへの移動
サイドバーの開閉や Claude Code のペインへの移動は、キー操作で行います。
- サイドバーを開閉する(
prefix + b) - 選択したセッションのペインへ移動する(
Enterかクリック) - 一覧をポップアップで開く(
prefix + B) - 次(前)の完了か承認待ちのセッションへ移動する(
prefix + C-n、prefix + C-p)
表示とキーの設定
表示の言語やサイドバーを開閉するキーは、tmux のオプションで変更できます。
| オプション | 既定値 | 内容 |
|---|---|---|
@cts_lang |
en |
表示の言語(en か ja) |
@cts_help |
on |
サイドバーの下端にキー操作のヘルプを表示するか |
@cts_toggle_key |
b |
サイドバーを開閉するキー |
筆者は prefix を C-t にしており、C-t を 2 回押すとサイドバーを開閉するようにしています。
set -g prefix C-t
# C-t そのものは prefix + t で送信する
bind t send-prefix
set -g @cts_toggle_key C-t
ほかのオプションはREADMEを参照してください。
どのように実装したか
プラグインの構成
本体は 1 つの Rust のバイナリ claude-tmux-sidebar で、呼び出し元ごとにサブコマンドを分けています。
hookは Claude Code の hook から呼び出され、セッションごとの状態ファイルを更新するtoggle・ensure・fix-widthなどは tmux のキー操作や hook から呼び出され、サイドバーのペインの開閉や、幅とレイアウトの維持を担当するsidebarはサイドバーのペインで常駐し、状態ファイルを読み込んで一覧を描画する
hook とサイドバーは直接通信せず、JSON の状態ファイルを介して状態を共有しています。状態ファイルの保存先は ~/.cache/claude-tmux-sidebar/ です($XDG_CACHE_HOME があればその下)。
Claude Code の hooks で通知されるイベントと状態の対応
Claude Code の hooks は、セッションの開始やプロンプトの送信、ツールの実行前後などのイベントで任意のコマンドを実行する仕組みです。コマンドには、イベントの内容が JSON の payload として標準入力から渡されます。
claude-tmux-sidebar では、次のイベントでシェルスクリプト bin/cts-hook が実行されるようにしました。bin/cts-hook はバイナリの hook サブコマンドを起動します。
SessionStart SessionEnd UserPromptSubmit PreToolUse
PostToolUse PermissionRequest Notification Stop
payload のうち使用しているのは、イベント名・セッション ID・作業ディレクトリ・ツール名・通知の種類です。
イベントから状態への対応は src/hook/transition.rs の step 関数にまとめています。
fn step<'a>(event: &Event<'a>, prev: Option<Status>) -> Step<'a> {
match *event {
Event::SessionStart => Step::To(prev.unwrap_or_default(), ""),
Event::SessionEnd => Step::Remove,
Event::UserPromptSubmit => Step::To(Status::Running, ""),
Event::PreToolUse { tool } if tool == ASK_TOOL => Step::To(Status::Waiting, ASK_DETAIL),
Event::PreToolUse { tool } | Event::PostToolUse { tool } => Step::To(Status::Running, tool),
Event::PermissionRequest { tool } => Step::To(Status::Waiting, tool),
Event::Notification { kind } => notification(kind, prev),
Event::Stop => Step::To(Status::Done, ""),
Event::Other => Step::Ignore,
}
}
SessionStart では直前の状態を引き継ぎ、状態ファイルが無ければ待機にします。AskUserQuestion で Claude が質問しているときも、利用者が回答するまで処理が進まないので承認待ちにしています。
Notification は通知の種類で分岐しており、permission_prompt と elicitation_dialog は承認待ちにします。Esc でターンを中断すると Stop の hook が実行されず、そのセッションは稼働中のまま残ります。そこで、入力が無いまま 60 秒ほど経過すると通知される idle_prompt を受けて、稼働中のセッションを待機に戻しています。
状態ファイルに記録する情報
状態ファイルはセッションごとに 1 つ作成し、サイドバーの表示とペインへの移動、終了したセッションの判定に使用する情報を記録します。
| フィールド | 用途 |
|---|---|
session_id |
Claude Code のセッション ID。状態ファイルの名前にも使用する |
state |
状態(稼働中・承認待ち・完了・待機) |
detail |
稼働中と承認待ちのときに表示するツールの名前 |
cwd |
作業ディレクトリ。サイドバーに表示するセッションの名前に使用する |
pane |
Claude Code が動作している tmux のペイン($TMUX_PANE)。ペインへの移動と、終了したセッションの判定に使用する |
pid |
Claude Code のプロセスの PID。終了したセッションの判定に使用する |
since |
状態が変化した時刻。経過時間の表示に使用する |
状態ファイルには作業ディレクトリやツール名が含まれますが、claude-tmux-sidebar は状態ファイルを外部に送信しません。状態ファイルとそのディレクトリは、本人以外が読み取れない権限で作成しています。
サイドバーが書き込み途中のファイルを読み込まないよう、状態ファイルは一時ファイルに書き込んでから rename で置き換えています。
また、hook の失敗で Claude Code を停止させてはいけないので、hook のエラーはすべて無視し、常に正常終了するようにしています。
PID を使った終了済みセッションの状態ファイルの削除
SessionEnd が通知されないまま Claude Code のプロセスが終了すると、状態ファイルが残り、終了したセッションがサイドバーに表示され続けます。そこで hook では、親プロセスを ps でさかのぼって claude のプロセスを探索し、その PID を状態ファイルに記録しています。
サイドバーは状態ファイルを読み込むたびに、状態ファイルに記録した PID のプロセスとペインが存在するかを確認し、どちらかが存在しなければ状態ファイルを削除します。
The Elm Architecture に沿ったサイドバーの状態管理
サイドバーの状態管理は、The Elm Architecture(TEA)に近い形で実装しました。中心にあるのは update(model, msg) -> (model, effects) という関数で、現在の状態と入力から、次の状態と実行すべき副作用を返します。
入力のうちスナップショットは、tmux のペインの一覧やフォーカスと、状態ファイルから読み込んだセッションをまとめたものです。
サイドバーは、画面を描画してから最大 150 ms 入力を待機する処理を繰り返しています。そのたびに状態ファイルのディレクトリの更新時刻を確認し、更新時刻が変化したときか、前回から 500 ms 経過したときにスナップショットを読み込み直します。そのため、hook が書き込んだ変化は 150 ms ほどでサイドバーに反映されます。
副作用は「このペインへ移動する」「サイドバーを閉じる」のような値で表し、tmux のコマンドを実際に実行する処理は effect モジュールに集約しました。
サイドバー以外でも、hook の状態遷移(hook::transition)やレイアウトの判定(control::layout)は、値を受け取って値を返す関数にしています。tmux の操作は tmux モジュール、状態ファイルの読み書きは session::store モジュール、ほかのプロセスの確認は process モジュールに集約しました。副作用の無い関数は、tmux を起動せずに単体テストできます。
tmux でのサイドバーのペインの管理
サイドバーは、各ウィンドウの左端に表示する、ウィンドウの高さいっぱいのペインです。split-window -hbfd でウィンドウの左側にペインを作成し、ペイン単位のオプションでマークして通常のペインと区別しています。
サイドバーを開いている間は、すべてのウィンドウに表示しています。新しいウィンドウやセッションを作成したときと、ウィンドウやセッションを切り替えたときに tmux の hook から ensure を呼び出し、そのウィンドウにサイドバーが無ければ表示します。サイドバーを追加すると zoom が解除されてしまうので、zoom 中のウィンドウには表示しません。
レイアウトが変化したときやクライアントの大きさが変化したときは、fix-width でサイドバーの幅を設定値に戻しています。
以前は、サイドバーの開閉を繰り返すとペインの幅が少しずつ狭くなる不具合がありました。そこで、サイドバーを開く前にウィンドウのレイアウトを保存し、閉じるときに select-layout で開く前のレイアウトへ復元するようにしています。
bash と Python から Rust への書き直し
Rust を採用した理由
当初は bash と Python で実装しており、hook は bash と jq で payload を読み込み、サイドバーは Python の curses で描画していました。
この構成で問題はありませんでしたが、開発を進めるにつれて bash と Python のコードの見通しが悪くなっていきました。さらに、ビルド済みのバイナリを配布すれば、実行時に Python と jq が不要になります。そこで Rust で書き直し、1 つのバイナリにまとめました。
Rust に書き直した後の hook の処理時間
hook は、ツールを 1 回実行するたびに PreToolUse と PostToolUse の 2 回実行されます。bin/cts-hook から起動したときの 1 回あたりの処理時間は、当初は 18 ms ほどかかっていましたが、Rust への書き直しによって 7 ms ほどに短縮できました。
書き直す前は 1 回の hook で jq を 2〜4 回起動しており、jq の起動には 1 回あたり 2.1 ms ほどかかっていました。書き直した後のバイナリ単体の処理時間は 1 ms ほどで、残りの 6 ms ほどはシェルと bin/cts-hook の起動にかかっています。
Rust のツールチェーン無しで使用できるバイナリの配布
Rust 製のプラグインをソースのまま配布すると、利用者が手元でビルドする必要があります。そこで、macOS と Linux 向けのバイナリを GitHub Actions でビルドし、GitHub Release に公開しています。
バイナリは、tmux がプラグインを読み込むたびに scripts/fetch-binary.sh が自動で取得します。プラグインのバージョンと同じバイナリが無ければ Release からダウンロードするので、プラグインを更新するとバイナリも新しいバージョンに更新されます。自分の環境向けのバイナリを取得できなかったときは、cargo build で手元でビルドします(この場合は Rust のツールチェーンが必要です)。
また、ダウンロードやビルドの間 Claude Code を待機させないようにしています。Claude Code の hook から呼び出す bin/cts-hook は、バイナリが無いときはインストールをバックグラウンドで開始し、そのイベントは処理せず、即座に終了します。
dir=$(cd "$(dirname "$0")" && pwd)
bin="$dir/claude-tmux-sidebar"
[ -x "$bin" ] && exec "$bin" hook
nohup "$dir/../scripts/fetch-binary.sh" >/dev/null 2>&1 &
exit 0
サイドバーの表示が期待と違うときのトラブルシューティング
起こりそうな問題とその原因、対処を次の表に整理しました。困ったときに参照してください。
| 症状 | 原因 | 対処 |
|---|---|---|
| 中断したセッションが「稼働中」のまま残る | Esc でターンを中断すると、Stop の hook が実行されない |
60 秒ほど待機する。idle_prompt の通知を受けた時点で「待機」に戻る |
| zoom 中のウィンドウにサイドバーが表示されない | 表示すると zoom が解除されてしまうので、zoom 中のウィンドウには表示しない | zoom を解除してから、prefix + b を押すか、別のウィンドウに移動してから戻る |
| インストール直後に状態が表示されない | バイナリのダウンロードかビルドが完了するまでに発生した hook のイベントは、処理しない | 完了まで待機する。その後に発生したイベント(プロンプトの送信など)から表示される |
おわりに
この記事では、tmux のサイドバーに Claude Code の状態を一覧表示する自作のプラグイン claude-tmux-sidebar を紹介しました。このプラグインによって、ウィンドウを切り替えなくても承認待ちや完了の Claude Code を把握できるようになりました。
よかったらclaude-tmux-sidebarを使ってみてください。要望があればIssueに気軽に起票してください。