# ターミナル 1 枚に AI 開発コックピットを建てる — Claude / File / GitHub を tmux に畳んだ常駐環境

> SSH でログインしたら、いつも同じ作業場にそのまま着地する——Claude Code を何本も並行で回しながら、ファイルも PR も同じ場所で行き来できる「常駐の開発コックピット」を tmux で組んだ記録。1 枚の tmux セッションに Claude（Agent View）/ File（lf ファイラー）/ GitHub（PR ブラウザ）の 3 ウィンドウを畳み、Shift+矢印で回す。再アタッチで最新化する自己修復、gh-dash をやめて gh + fzf の自前ブラウザに置き換えた判断、そして下段ステータスバーに要対応セッションを型別で光らせるクロスセッション通知まで、実際のスクリプトと公式仕様の裏取りつきで、一次情報として残します。

- 出典: SUZ-LAB（実験ログ / 一次情報）
- URL: https://suz-lab.co.jp/logs/ai-cockpit-cross-session-notifications
- 公開: 2026-07-12
- 更新: 2026-07-12
- 著者: Hiro（主任研究員・https://suz-lab.co.jp/researchers/hiro・X: https://x.com/suz_lab_hiro）
- 柱: テクノロジー
- タグ: #tmux #Claude Code #SSH #lf #gh #DevContainer #マルチエージェント #hooks #AI
- カバー画像: https://suz-lab.co.jp/assets/logs/ai-cockpit-cross-session-notifications.png?v=19dc49e8

---


## 何をしたかったか — 繋いだ先に欲しいのは「常駐の作業場」

このリポジトリは Codespaces 上の Dev コンテナで回している。母艦には SSH でログインして
入る。繋ぐところまでは整っていた。けれど実際に手を動かし始めると、**繋いだ先に欲しいものが
「素のシェル 1 枚」では足りない**ことがすぐ分かった。

行動原則の一つは「AI を使い倒す」だ。使い倒すとは、1 本の対話を丁寧に回すことではなく、
**別々の関心事に Claude Code を何本も並行で走らせ、その合間にファイルを覗き、PR を捌く**
ことだった。そのたびに `claude`・`lf`・`gh` を手で立ち上げ直し、どこで何をしていたかを
思い出す——この摩擦が、並行で回す旨みを削っていた。

だから狙いを一つに定めた。**SSH ログインしたら、いつも同じ「作業場」にそのまま着地する。**
Claude を回すところ、ファイルを見るところ、PR を捌くところが、最初から 1 枚のターミナルに
畳まれていて、行き来するだけでいい。永続セッションなので、切れて戻っても同じ配置が待っている。
このコックピットを `lab-view.sh` として組んだ。以下、その一つひとつを、代替案や実際の
つまずきと一緒に残す。

## 設計 — 1 セッション・3 ウィンドウ・矢印キーで回す

コックピットの骨格はシンプルだ。**1 つの tmux セッションに 3 つのウィンドウ**を並べ、
ウィンドウ名をそのまま下段メニューの表記にする。

- **Claude** — `claude agents`（Agent View）を全画面で常駐。ログイン直後はここに着地する。
- **File** — [lf](https://github.com/gokcehan/lf) ファイラー。
- **GitHub** — `gh` + [fzf](https://github.com/junegunn/fzf) の PR ブラウザ（`gh-view.sh`）。

この骨格を成り立たせるために、いくつか地味な判断を重ねた。

- **専用ソケットで隔離する。** 既定の tmux サーバを使うと、汎用セッション名や prefix 無しの
  設定が他の用途とぶつかる。コックピットは専用ソケット（`-L suzlab`）に隔離し、その上で
  セッション名を `lab` にする。キー設定やステータスバーの変更が他の tmux へ漏れない。
- **ウィンドウ名 = メニュー表記。** 下段メニューはウィンドウ名（`#W`）を index 順に並べる。
  だから表示したい表記（Claude / File / GitHub）をそのままウィンドウ名に採り、Shift+矢印の
  切替対象や自己修復の grep も同じ名前で回す。主役の Claude を最左（index 0）に作り、
  ログイン直後のフォーカスをそこへ置く。
- **切替は Shift + ←/→。** 「左右に移動」という直感に合い、修飾キー付きなので素の矢印を
  奪わない（矢印は lf / Claude / fzf が中で使う）。Claude → File → GitHub の循環にした。
- **セッションは無いときだけ作り、キー設定と GitHub ウィンドウは毎回当て直す。** セッション
  本体を毎回作り直すと再接続で状態が飛ぶ。一方、キーバインドや GitHub ウィンドウの用意を
  作成分岐の中だけに置くと、後から入れた fzf や設定変更が既存セッションに反映されない。
  そこで**本体は冪等に温存・付帯設定は毎回適用**に分け、再アタッチのたびに最新化される
  ようにした（自己修復）。

## Claude ウィンドウ — Agent View を常駐させる

主役のウィンドウは `claude agents`（Agent View）を全画面で開いたまま常駐させる。複数の
Claude Code セッションの一覧・切替はここが担う。ログイン直後にここへ着地するので、「開いたら
すぐ AI に投げられる」——同じ作業場に着地するという狙いが、そのまま作業場の入口になった。

Claude Code には Agent View / Agent Teams のような**複数エージェントを束ねる native な導線**が
育ちつつある（[docs](https://code.claude.com/docs/en/agent-view)）。コックピットは、それを
1 枚のターミナルの中心に据えつつ、周りにファイルと PR の窓を足した恰好だ。

## File ウィンドウ — lf に bat プレビューを噛ませる

ファイルを覗く窓は lf にした。既定で「左＝ファイルリスト／右＝プレビュー」の 2 列を出す
（追加設定なし）。プレビューは bat でシンタックスハイライトと行番号つきにし、Enter で
ファイル全体をページャ表示、← でファイラーに戻る、という往復にした。

ターミナルファイラーは ranger / [yazi](https://github.com/sxyazi/yazi) / nnn と選択肢が多い
（2026 年の比較では、画像プレビューが素で効く・非同期でUIが固まらない yazi を新規に薦める
[声もある](https://mqdir.com/blog/file-management/nnn-vs-yazi)）。それでも lf を採ったのは、
**単一バイナリで依存が軽く、既定のまま 2 列プレビューが出て、Codespaces に置くのが楽**
だったからだ。速さや多機能でなく、「この環境に薄く乗る」を優先した。

## GitHub ウィンドウ — gh-dash をやめて gh + fzf にした

PR を捌く窓は、最初 [gh-dash](https://github.com/dlvhdr/gh-dash) を使うつもりだった。GitHub CLI
拡張の TUI ダッシュボードで、repo 別セクション・Vim キー・YAML 設定を備えた成熟したツールだ
（〜12k stars）。ところが Codespaces で詰まった。**既定の `GITHUB_TOKEN` では
`branchProtectionRules` を読む権限が無く、PR 一覧の取得がそこで固まる。**

環境側のトークン権限に依存する不安定さを持ち込みたくなかったので、gh-dash はやめて、
`gh pr list` と `gh pr view` / `gh pr diff`（`branchProtectionRules` を要求しない）を叩いて
fzf で選ぶだけの薄い自前ブラウザ（`gh-view.sh`）に置き換えた。**成熟した汎用ツールより、この
環境で確実に動く薄い自前**を採る——ガードレールの監査で身につけた判断を、ここでも使った。

自前にしたぶん、細かい挙動も自分で決められる。gh-view の fzf は自分のイベント（起動・Ctrl-R・
詳細/diff を閉じた後）でしか一覧を取り直さないので、tmux のウィンドウ切替で GitHub に戻っただけ
では表示が古いまま残る。そこで **GitHub ウィンドウへ遷移したら、その場で fzf に reload（Ctrl-R）を
送って最新化**する tmux バインドを足した。条件を「window_name==GitHub かつ
pane_current_command==fzf」に絞り、pager（less）や shell が前面のときには送らない。

## 下段の一機能 — 要対応セッションを型別で光らせる

3 ウィンドウを行き来していると、地味だが致命的な問題が出る。**別ウィンドウにいるあいだ、
裏で止まった Claude セッションに気づけない。** Claude は権限確認や質問で頻繁に止まり、
こちらが入力を返すまで待つ。File や GitHub を見ているあいだ、それが放置される。

そこでコックピットの**下段ステータスバーに、要対応セッション数を持続表示**する一機能を足した。
先行事例（[Marc Nuri](https://blog.marcnuri.com/ai-coding-agent-dashboard) は「5〜10 並行で手に
負えなくなる」と書き、**人間が要るイベントだけ通知して alert fatigue を避ける**ことを勧める）に
倣い、「要対応だけを、緊急度で型分けして出す」を原則にした。

| 型 | 意味 | いつ消えるべきか | 表示 |
| --- | --- | --- | --- |
| **入力待ち** | あなたの入力を待って Claude が止まっている | 対応する（プロンプト送信）まで**持続** | 赤・太字・**点滅** |
| **完了** | 回答・結果が出そろって準備 OK | 開いて読めばよい＝**時間で自動失効**（既定 10 分） | 緑・**定常** |

**状態を捕まえる入口は [Claude Code の hooks](https://code.claude.com/docs/en/hooks) だ。**
フックには stdin で JSON が渡り、その中の `session_id` がセッションを一意に識別する。
Notification→入力待ち / Stop→完了 / UserPromptSubmit→クリア / SessionEnd→残骸掃除、の 4 つを
`~/.claude-notify/<session_id>.<type>` というマーカーファイルの作成・削除に落とす。表示側は
tmux の `status-right` で `#(claude-notify-status.sh)` を呼び、`.wait` / `.done` を型別に数えて
色付き文字列を返す（0 件なら空文字＝時計だけ）。`status-interval 5` の再描画が、点滅を数秒周期の
ちらつきとして見せる。

この一機能を作るなかで、公式仕様の裏取りが 2 つ効いた。

- **フックは何があっても `exit 0` で抜ける。** 公式ドキュメントによれば `exit 2` は
  ブロッキングエラーで、しかも**ブロックできるイベントに `Stop` と `UserPromptSubmit` が
  含まれる**（`Notification` と `SessionEnd` は exit code が無視される）。つまり通知の都合で
  フックが非ゼロで落ちると、**セッション本体を妨害しかねない**。通知が主役の邪魔をしては
  本末転倒なので、全経路 `exit 0` を徹底した。作法ではなく仕様上の必然だ。
- **緑背景バッジ上の `blink` は背景ごと点滅する。** 当初は完了も点滅させていたが、端末に
  よっては緑背景のステータスバー上で `blink` を使うと文字だけでなくセルの背景まで点滅する。
  SGR の `blink` はセル単位の属性で、「背景据え置き・文字だけ点滅」を SGR だけで表す術が無い。
  外部に裏の取れない**こちらの手元の観察**だが、だからこそ残す——完了は点滅をやめて定常にした。
  結果、「点滅＝要対応・定常＝準備 OK」という素直な区別に落ち着いた。

なお同じ「どのセッションが待っているか」を、[デスクトップ通知＋ペインへジャンプ](https://quemy.info/2025-08-04-notification-system-tmux-claude.html)で
解く先行事例や、[`claude agents --json` を source of truth](https://www.devas.life/i-made-a-claude-code-session-manager-for-tmux/)にして
fzf で並べる例もある。コックピットが選んだのは**割り込まない・依存を足さない・視界の端に
常駐する**「下段バッジ」だ。前面を奪わず、作業中の集中を切らさずに「今 2 つ待っている」が分かる。

## つまずき

- **待機中に閉じたセッションの `.wait` が残り続ける。** 入力待ちのまま閉じると、素朴な実装では
  マーカーが安全弁の掃除まで残り「入力待ちでないのに ● 入力待ち が出続ける」。`SessionEnd` で
  入力待ちの残骸だけを消して塞いだ（完了表示は残す）。
- **`session_id` が取れないとき固定名に集約してはいけない。** `unknown` のような 1 名に寄せると
  複数セッションが 1 件に衝突してカウントが狂う。誤通知を避けて no-op にした。
- **GitHub ウィンドウが古いまま。** 他ウィンドウから戻ると fzf の一覧が更新されない → 遷移時に
  reload を送って最新化（前述）。
- **新規作成時に GitHub に着地してしまう。** `new-window` は既定でフォーカスを奪うので、GitHub を
  `-d` で裏に用意し、ログイン直後のフォーカスを Claude に残した。

## 検証

- **ログイン → 着地。** SSH ログインで tmux セッション（`lab`）に自動アタッチし、Claude Agent
  View に着地。Shift+←/→ で Claude → File → GitHub を循環できること。
- **自己修復。** 再アタッチ時に GitHub ウィンドウ・キーバインド・fzf 最新化が効くこと。gh / fzf /
  認証が揃わない環境では GitHub ウィンドウを作らず素直に落ちること。
- **通知の往復。** Claude を止める → 下段に `● 入力待ち 1`（赤点滅）→ プロンプト送信で消える。
  応答完了 → `✓ 完了 1`（緑定常）→ 10 分放置で自動失効。2 本止めれば `● 入力待ち 2`。別ウィンドウに
  いても下段に出続けること。
- 触れたのは devcontainer 側のスクリプトと `.claude/settings.json` なので、アプリのビルドには
  影響しない。リポジトリ全体で `pnpm typecheck` / `pnpm lint` / `pnpm format:check` が通ること。

## 学び

- **作業場は「配置」で決まる。** 速いエディタや多機能なファイラーより、「開いたらいつも同じ 3 枚が
  同じ場所にある」ことのほうが、並行で回す摩擦をずっと減らした。いつも同じ作業場に着地するという
  狙いは、結局この地味な定位置化で達成された。
- **成熟した汎用より、この環境で確実に動く薄い自前。** gh-dash を落として gh + fzf にしたのも、
  lf を選んだのも、通知を割り込まない常時バッジにしたのも、根は同じだ。SSH で入る Codespaces
  という制約から逆算すると、選択は自ずと絞れる。
- **端末の制約が、設計をより素直にすることがある。** 完了も点滅させたかったが、緑背景の都合で
  定常にした。結果、最初の型分けより分かりやすい表現に落ち着いた。制約は必ずしも妥協ではない。
- **裏取りは「なんとなくの安全策」を「必然」に変える。** フックの `exit 0` は、公式仕様を当たって
  初めて「`Stop` / `UserPromptSubmit` を守るための必然」だと分かった。一次情報として書くと決めると、
  自分の実装の理由まで確かめ直すことになる。

