オレのClaude Code作業環境、控えめにいって最高すぎる〜Stream Deckでherdrを操作、完了はずんだもんが読み上げ〜

武井でございます。

手元のボタンを見るだけで、どの Claude Code が作業中で、どれが確認待ちで、どれが終わったのかが一目で分かる。

ボタンを押せば、そのターミナルに一発で飛べる。

作業が終わったら、ずんだもんが「何をやって、次に何をすればいいか」を声で教えてくれる。

そんな作業環境をStream Deckとherdr、ローカルLLM、そしてVOICEVOX(ずんだもん)で作りました。まずは動いているところを見てください。

この環境でできることは、次の 3 つです。

  • 全タブの状態がボタンで分かる:ボタンの色と、ボタンに住んでいるドット絵のキャラクターの動きで、Claude Codeのタスクの作業中・確認待ち・完了が分かる

  • ボタンを押すとそのタブに飛べる:ターミナルをたくさん開いていても、ボタン一つでそのターミナルにとべる。またターミナルが別のデスクトップにあっても、そこへ移動してタブまで切り替わる

  • 終わったらずんだもんが教えてくれる:回答が終わったときや確認待ちになったときに、「何をしたか・どうなったか・次に何をすればいいか」をずんだもんが読み上げる

以下、なぜ作ったのか、どういう仕組みなのかを順に紹介します。

なぜ作ったのか

AI エージェントで開発するようになってから、ターミナルを開く数が一気に増えました。Claude Code に 1 つのタスクを任せている間に、別のターミナルで別のタスクを任せる。気づけば 5 個、10 個とターミナルが並んでいます。まぁ、以下はちょっと大げさですが。。。

 

こうなると、困ることが 2 つ出てきます。

  • どれがどういう状態か分からない:どのターミナルが確認待ちで止まっていて、どれが終わったのか、1 つずつ見に行かないと分からない。確認待ちのまま何十分も放置していた、ということがよく起きる
  • ターミナルの行き来が面倒:ウィンドウやタブを探して切り替えるのが地味に手間。別のデスクトップに置いていると、さらに面倒

これを、手元のデバイスと音声で解決しようというのが今回の仕組みです。

全体像

登場人物は次の 5 つです。

 

Stream Deck のプラグインが真ん中にいて、herdr の状態を見張っています。図の番号に沿って、処理の流れを説明します。

① ユーザーが herdr のターミナルで、Claude Code に作業を依頼します。

② herdr の中で動いている Claude Code が、Claude とやり取りしながら作業を進めます。

③ Stream Deck プラグインが、1.2 秒ごとに herdr の CLI を呼んで、スペース・タブ・ペインの一覧を問い合わせます。作業が終わったタブや確認待ちのタブを見つけたときは、そのタブの画面の中身も読みに行きます(herdr pane read)。ボタンが押されたときは、herdr にそのタブへの切り替えを頼みます。

④ herdr が、各タブのエージェントの状態(作業中・確認待ち・完了・待機中)とセッション ID、頼まれたタブの画面の中身を返します。

⑤ プラグインが、作業が終わったタブや確認待ちのタブの画面をローカル LLM に渡して、読み上げ用の文章を作るよう頼みます(Chat Completions)。

⑥ ローカル LLM が、「何をしたか・どうなったか・次に何をすればいいか」をずんだもんの口調にまとめた文章を返します。

⑦ プラグインが、その文章を VOICEVOX に渡して、ずんだもんの声にするよう頼みます。

⑧ VOICEVOX が、ずんだもんの声の音声データを返します。

⑨ プラグインが、各タブの状態をもとにボタンとインフォバーの画像を描き、Stream Deck に表示します。

⑩ プラグインが、⑧ で受け取った音声をスピーカーで再生します。

ここからは、それぞれの部品を紹介します。

herdr:AI エージェントのためのターミナル管理ツール

herdr は、AI コーディングエージェント向けのターミナルマルチプレクサです。tmux のように、1 つのターミナルの中で複数の作業場所を切り替えられます。

作業場所は「スペース」と「タブ」の 2 段で管理します。プロジェクトごとにスペースを作り、その中にタブを並べるイメージです。

herdr の良いところは、各タブで動いているエージェントの状態を把握してくれることです。
状態は working(作業中)、blocked(確認待ち)、done(完了)、idle(待機中)の 4 つで、CLI から JSON で取得できます。

herdr pane list
{
  "id": "cli:pane:list",
  "result": {
    "panes": [
      {
        "pane_id": "w1:p1",
        "tab_id": "w1:t1",
        "agent": "claude",
        "agent_status": "idle",
        "agent_session": { "value": "d4cd505c-..." },
        "cwd": "/Users/ntakei/data/tmp/20260921/書籍PDF"
      }
    ]
  }
}

今回のプラグインは、この情報を 1.2 秒ごとに取りに行って、ボタンの表示に使っています。

Stream Deck Neo:ボタンに何でも表示できるランチャー

Stream Deck は Elgato が出しているデバイスで、ボタンの一つひとつが小さな液晶になっています。ボタンを押すとアプリを起動したり、ショートカットを送ったりできる、いわゆるランチャーです。配信者の定番デバイスですが、開発者が使っても便利です。

今回使ったのは Stream Deck Neo です。ボタンが 8 個(4 個 × 2 段)あり、その下に「インフォバー」と呼ばれる横長の小さな画面があります。インフォバーの両側には、ページを切り替えるタッチボタンが付いています。

Stream Deck はプラグインを自作できます。プラグインは Node.js で動き、ボタンに表示する画像も自由に作れます。つまり、ボタンに何を表示して、押したら何をするかを自分で決められるということです。これを使って、herdr 専用の操作パネルを作りました。

自作プラグイン:herdr 専用の操作パネル

ボタンの配置

上段の 4 つのボタンがスペース、下段の 4 つのボタンが、上段で選んだスペースのタブです。

  • 上段のスペースを押すと、下段がそのスペースのタブに切り替わる
  • 下段のタブを押すと、herdr でそのタブに切り替わる
  • スペースが 5 つ以上あるときは、インフォバー横のボタンで Stream Deck のページを切り替える
  • 1 つのスペースにタブが 5 つ以上あるときは、選択中のスペースボタンをもう一度押すと、下段が次の 4 タブに切り替わる

ボタンに住むドット絵のキャラクター

各タブのボタンには、ドット絵のキャラクターが住んでいます。キャラクターは、そのタブのエージェントの状態を演じます。

 

※ プラグインがボタンに描いている絵を、そのまま画像にしたものです。

上段のスペースのボタンには、そのスペースにいるキャラクターの顔が並びます。スペースの中に入らなくても、「このスペースで誰かが手を振っている(確認待ち)」と分かります。

ボタンの画像は、プラグインが SVG で描いて 150 ミリ秒ごとに差し替えています。絵が前回と同じなら送らないようにして、負荷を抑えています。

選択中のボタンは明るく

今選んでいるスペースとタブは、ボタンの背景が明るくなります。最初は白い枠で囲んでいましたが、小さなボタンでは分かりにくかったので、背景そのものを状態の色で明るく塗るようにしました。選んでいないボタンは暗いままなので、明るいボタンが 1 つだけ目に入ります。

押したらそのターミナルへ飛ぶ

ボタンを押すと、herdr のタブを切り替えたうえで、herdr が動いているターミナルアプリ(今回は Ghostty)を前面に出します。macOS はアプリを前面に出すと、そのウィンドウがあるデスクトップ(Space)へ自動で切り替えてくれます。なので、ターミナルを別のデスクトップに置いていても、ボタン 1 つでそこへ飛べます。

インフォバーにコンテキスト残量と利用上限

ボタンの下のインフォバーには、次の 3 つを表示しています。

  • 選んでいるタブの Claude Code のコンテキスト使用率と、その内訳
  • 5 時間の利用上限の使用率と、リセットされる時刻
  • 1 週間の利用上限の使用率と、リセットされる時刻

実機のインフォバーは、こんな感じです。

実機の画面は小さいので、それぞれ何を表しているのかを図に起こしました。

※ 説明のために、プラグインと同じ描画コードで大きく描き直したものです。数値は写真とは別のときのものです。

どのタブの Claude Code かは、herdr がペインごとに持っているセッション ID で特定しています。使用率は、Claude Code の /usage と /context を、claude -p で裏から実行して取っています。

ずんだもんが読み上げてくれる仕組み

ここからが一番気に入っているところです。

流れ

① プラグインが、1.2 秒ごとに herdr へ各タブの状態を問い合わせます。

② herdr が各タブの状態を返します。プラグインは、前回から「作業中 → 完了」や「→ 確認待ち」に変わったタブを見つけます。

③ プラグインが、そのタブの画面を読みます(herdr pane read で直近 200 行)。

④ herdr が画面の文字を返します。枠線や記号も混ざった、ターミナルの表示そのままです。

⑤ プラグインが、画面の文字と「何をした → どうなった → 次にすること」の順で伝えるよう指示した文章を、ローカル LLM(Ollama の qwen3.5:9b)に渡します(Chat Completions)。

⑥ ローカル LLM が、ずんだもんの口調の読み上げ文を返します。

⑦ プラグインが、読み上げ文を VOICEVOX(Docker で起動)に渡します。話者はずんだもんです。

⑧ VOICEVOX が音声データ(WAV)を返します。

⑨ プラグインが、音声をスピーカーで再生します(afplay)。

どのスペースのタブでも読み上げます。文の頭に「業務自動化から。」のようにスペース名が付くので、どこの話か分かります。

ただ要約するのではなく「次に何をすればいいか」を伝える

LLM に渡すプロンプトで一番工夫したのは、「要約して」とは頼んでいないことです。画面を見ていない人が、声だけを聞いて次に何をすればいいか分かることを最優先にしてもらっています。

完了したときは、次の 3 つを 3 文で伝えてもらいます。

  1. このセッションで何をしたか
  2. その結果どうなったか
  3. 次にユーザーがすべきこと(質問や選択肢があれば、その中身)

確認待ちのときは、次の 3 つです。

  1. 今何の作業をしている途中か
  2. 何の許可を求めているのか、何を質問しているのか(削除などの取り消しにくい操作は必ず言う)
  3. どう答えればいいか(選択肢は読み上げるが、どれを選ぶべきかは勧めない)

入力はターミナルの画面そのままなので、枠線や記号、入力欄なども混ざっています。それらは LLM に「無視して中身だけ読み取って」と伝えています。

実際の読み上げ

実際に読み上げられた文章です。

作業が終わったとき:

業務自動化から。未入力の工数を確認して割り振り案を出したのだ。6 日分を指定されたプロジェクトに正しく登録して集計も一致したのだ。月末の 2 日分を入力するか、期限までにどうするか確認してほしいのだ。

確認待ちのとき(コマンドの実行許可を求められた場面):

テストを直すために依存パッケージを再インストールしてテストを流す作業の途中なのだ。node_modules を削除して依存関係を再構築し、テストを再実行するコマンドの許可を求めているのだ。許可する、今後も聞かずに許可する、やめて指示し直す、のどれかを選んでほしいのだ。

「削除する」ことをちゃんと伝えてくれて、選択肢も読み上げてくれます。これならターミナルを見に行く前に、答えを決められます。

このプラグインを作ったプロンプト

このプラグインは、Claude Code に作ってもらいました。ソースコードは公開していませんが、代わりに「これを渡せば同じものが作れる」プロンプトを紹介します。

実際のやり取りは試行錯誤の連続だったので、完成したコードから逆算して、最初から渡しておけばよかった内容にまとめ直しました。

プロンプトは 3 つに分けています。1 回で全部を頼むより、段階ごとに実機で動きを確かめながら進めた方が、手戻りが少なく済みます。パスやアプリ名など、環境に依存するところは自分の環境に合わせて書き換えてください。

ステップ 1:ボタンでスペースとタブを操作する

まずは土台です。herdr の状態をボタンに表示して、押したらそのタブに切り替わるところまでを作ります。

Stream Deck Neo(macOS)から herdr を操作するプラグインを TypeScript で作ってください。
SDK は @elgato/streamdeck 3.x、ビルドは rollup、プラグインの UUID は jp.example.herdr にします。

# herdr について
- herdr は AI エージェント向けのターミナルマルチプレクサで、スペース(workspace)とタブで作業を管理する
- 次の CLI で状態を取れる。--json オプションは無く、最初から JSON を返す。
  結果は {"id": "...", "result": {...}} の形で包まれているので result を取り出すこと
  - herdr workspace list … workspace_id, label, focused, active_tab_id, agent_status
  - herdr tab list --workspace <id> … tab_id, label, agent_status(herdr の表示順で返るので並べ替えない)
  - herdr pane list … pane_id, tab_id, agent, agent_status, agent_session.value(セッション ID), cwd
  - herdr workspace focus <id> / herdr tab focus <tab_id> / herdr tab create --focus
- agent_status は working / blocked / done / idle。unknown などそれ以外は「エージェントなし」として扱う
- タブの状態は、そのタブのペインのうち最も深刻なもの(blocked > working > done > idle)にする

# ボタンの配置
- 上段 4 つ:スペース。押すとそのスペースを選び、herdr 側でもフォーカスする
- 下段 4 つ:上段で選んだスペースのタブ。押すとそのタブへ切り替える
- どのボタンが何番目を表示するかは、設定画面の「スロット」(1〜16)で決める。スロットは通し番号にして、
  Stream Deck のページ 2 に上段スロット 5〜8 を置けば、スペース 5〜8 を表示できるようにする
- 1 つのスペースにタブが 5 つ以上あるときは、選択中のスペースボタンをもう一度押すと、下段が次の 4 タブに切り替わる
  (最後まで行ったら最初に戻る)。選択中のスペースボタンには 1/2 のように今の位置を出す
- 押したときの緑のチェックマーク(showOk)は出さない。失敗したときだけ showAlert を出す

# 見た目
- ボタンの画像は 144×144 の SVG をコードで生成して setImage で送る
- 背景色で状態を表す:blocked 赤 / working 黄 / done ミント / idle 青 / エージェントなし 暗い紫
- 各タブのボタンには 14×12 マスのドット絵のキャラクターを置き、状態を演じさせる
  - working:机でタイピング(汗をかく) / blocked:手を振って「!」の吹き出し
  - done:バンザイして紙吹雪 / idle:居眠り(zzz) / エージェントなし:空席の椅子
- アニメーションは 150ms ごとに描き直す。タイマーは全ボタンで 1 本を共有し、前回と同じ画像なら送らない
- 上段のスペースボタンには、そのスペースのタブのキャラクターの顔を小さく並べる
- 名前は全角 4 文字がちょうど 1 行に入る大きさ(32px・太字 700)で、最大 2 行。
  少しだけはみ出す名前や、空白のない英単語は、折り返さずに文字を縮めて 1 行に収める。
  文字は真っ白にし、縁取りではなく右下にずらした薄い影で読みやすくする
- 選択中のボタン(選んでいるスペースと、herdr でアクティブなタブ)は、背景を状態色の明るい淡い色にして、
  文字を濃い紺にする。選んでいないボタンは暗くしない(次に押すボタンが読めなくなるため)

# ターミナルを前面に出す
- ボタンを押したら、herdr の切り替えの後に、herdr が動いているターミナルアプリを前面に出す
- ターミナルアプリは、tty を持つ herdr プロセスから ps で親をたどり、実行パスに .app/Contents/MacOS/ を含む
  最初のプロセスから探す(アプリ名は決め打ちしない)。見つけたら open -a <アプリのパス> で前面に出す
- macOS はアプリを前面に出すと、そのウィンドウがあるデスクトップへ自動で切り替えてくれる

# 注意点
- Stream Deck から起動したプロセスはシェルの PATH を持たない。herdr は /opt/homebrew/bin などを探して
  絶対パスで呼ぶ。設定画面でパスを指定することもできるようにする
- herdr へのポーリングは 1.2 秒に 1 回、全ボタンで 1 本だけにする
- manifest.json には CodePath を必ず書く。完成したら streamdeck validate でチェックする
- 設定画面(Property Inspector)は外部ライブラリを使わず、素の HTML と WebSocket で作る。
  select の値は文字列で保存されることがあるので、プラグイン側で数値に直す

ステップ 2:インフォバーにコンテキスト残量と利用上限を出す

次に、ボタンの下のインフォバーを作ります。

Stream Deck Neo のインフォバー(232×50)に、Claude Code の情報を表示するアクションを追加してください。
インフォバー全体を 1 つの pixmap にして、プラグインで描いた SVG を base64 の data URI で setFeedback に渡します。

# 表示する内容
- 上段:選んでいるタブの Claude Code のコンテキスト使用率(%)、内訳の積み上げバー、トークン数(例:636k/1m)
- 下段:5 時間の利用上限と 1 週間の利用上限の使用率(%)と、リセットされる時刻
- 使用率の色は、70% までミント、90% まで黄、それ以上は赤
- 文字は小さい画面でも読めるよう大きめにする(見出し 14px、% は 17〜18px)

# データの取り方
- 利用上限:claude -p "/usage" --output-format json --no-session-persistence
- コンテキスト:claude -p --resume <セッションID> "/context" --output-format json --no-session-persistence
- どちらも 60 秒ごと。/context は重いので、会話の記録ファイルの更新時刻が変わったときだけ実行する
- プローブ自身がセッションとして残らないよう、環境変数 CLAUDE_CODE_SESSION_NAME で名札を付けて見分ける

# 選んでいるタブのセッションの見つけ方
- herdr の pane list の agent_session.value にセッション ID が入っているので、まずそれを使う
- herdr が「エージェントなし」と言っているタブは、他の方法で推定せず「このタブは Claude なし」と表示する
  (同じディレクトリで別のタブの Claude が動いていても、それを拾わないようにするため)
- 会話の記録ファイルは ~/.claude/projects/<作業ディレクトリ>/<セッションID>.jsonl にある。
  ディレクトリ名は、作業ディレクトリのパスの英数字以外をすべて - に置き換えたもの(日本語も 1 文字ずつ - になる)。
  見つからなければ ~/.claude/projects の下をセッション ID で探す

ステップ 3:ずんだもんに読み上げさせる

最後に、読み上げの仕組みを足します。

Claude Code などのエージェントの作業が終わったときと、確認待ちになったときに、
その内容をローカル LLM で読み上げ用の文章にして、VOICEVOX のずんだもんに読み上げさせる機能を
このプラグインに追加してください。すべてのスペースのタブを対象にします。

# きっかけ
- herdr のタブの状態が working → done、または working → idle に変わったら「作業が終わった」とする
  (見ているタブの作業が終わると、herdr は done を経由せず idle になる)
- done → idle は「見た」だけなので読まない
- 状態が blocked に変わったら「確認待ち」とする
- プラグインの起動直後(前回の状態が無いとき)は読まない

# 流れ
1. そのタブのペインの画面を herdr pane read <pane_id> --source recent-unwrapped --lines 200 --format text で読む。
   直後は失敗することがあるので、間を空けて最大 3 回試す
2. ローカル LLM に Chat Completions(POST {URL}/chat/completions)で画面を渡し、読み上げ用の文章にしてもらう。
   thinking で出力を使い切らないよう reasoning_effort: "none" を付ける
3. VOICEVOX の /audio_query と /synthesis で音声にして、afplay で再生する
4. 文の頭に「<スペース名>から。」を付ける
- 読み上げは 1 件ずつ順番に。3 件より多く溜まったら古いものから捨てる。同じ画面は二度読まない

# LLM へのプロンプトで伝えること
- あなたはずんだもんで、画面を見ていないユーザーに音声で作業状況を伝える
- 入力はターミナル画面そのままなので、枠線・記号(❯ ⏺ ⎿ ▎ ─ │ など)・入力欄・ステータス行・切れた行は無視する。
  ❯ で始まる行がユーザーの依頼、⏺ で始まる行がエージェントの返答で、一番下が最新
- 聞き手が「次に自分が何をすればいいか」を分かることを最優先にする
- 作業が終わったとき:何をしたか → どうなったか → 次にユーザーがすべきこと(質問や選択肢があればその中身)を 3 文で
- 確認待ちのとき:何の作業中か → 何の許可・質問か → どう答えればいいか を 3 文で。
  削除・上書きなど取り消しにくい操作は必ず言う。選択肢は読み上げるが、どれを選ぶべきかは勧めない
- すべての文をずんだもんの口調(〜のだ、〜なのだ)で終える。前置きや敬語は使わない。
  コマンドやファイルパスはそのまま読まずに言い換える。120 文字以内
- 出力の例を 2〜3 個入れる

# 起動していないとき
- VOICEVOX やローカル LLM は起動し忘れることがある。読み上げ前に VOICEVOX の /version と LLM の /models に
  1.5 秒だけ問い合わせ、どちらかが応答しなければ、ユーザーに何も通知せずその回を飛ばして処理を続ける
- 読み上げた・飛ばした理由はプラグインのログにだけ残す

# 設定
- インフォバーの設定画面に、プラグイン全体で共通の設定(グローバル設定)として次を置く。空欄は既定値
  - 読み上げのオン・オフ(既定:オン)
  - LLM の URL(既定:http://127.0.0.1:11434/v1)とモデル(既定:qwen3.5:9b)
  - VOICEVOX の URL(既定:http://127.0.0.1:50021)と話者 ID(既定:3 = ずんだもん)

まとめ

herdr、Stream Deck、ローカル LLM、VOICEVOX を組み合わせて、AI エージェントとの並行作業をかなり快適にできました。

  • ボタンを見れば、全タブの状態が分かる
  • ボタンを押せば、そのターミナルへ飛べる
  • 終わったら、ずんだもんが次にやることまで教えてくれる

仕事はずんだもんにまかせておいて、5時から男としゃれこみましょう。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

1人がこの投稿は役に立ったと言っています。
エンジニア募集中!

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です