セミナー会場の全員のClaude Codeと会話する — 進捗を集めて、席に行かずに指示を出す

開発claude-code-toolsアクティブ

商工会議所でセミナーをする。参加者20人がそれぞれのノートPCに Claude Code を入れて、それぞれのアカウントで動かしている。誰がどこまで進んだのかが分からない。手が止まっている人がいても、こちらからは見えない。結局、席をまわって画面を覗いて、キーボードを借りて打つことになる。

これを、講師のブラウザ1枚で済ませられる。参加者の Claude Code が現況を送ってきて、こちらから送った指示を受け取って動き出す。参加者は最初に1行貼るだけで、あとは何も操作しない。

以下は設計の記録で、まだ実装していない。しくみとしては成立するが、そのまま作ると危ない場所がいくつもあることも書いておく。

なぜハブをクラウドに置くのか

素直に考えると、講師のPCでサーバーを立てて、全員が同じ Wi-Fi にいるのだからそこへ送ればいい。実際、同じ仕組みを自宅の2台で動かしたときはそれで足りた(別記事に書いた)。

ところがこれは、会場の Wi-Fi ではまず動かない。商工会議所やホテル、カフェのゲスト回線は、同じ SSID につないだ端末どうしの通信を遮断する設定になっているのが普通だからだ。全員がインターネットには出られるのに、お互いだけは見えない。無線LANの設定画面では「プライバシーセパレータ」「クライアント分離」などと呼ばれている。

会場のWi-Fiが塞ぐのは端末どうしの直通で、外向きの通信は通ることが多い
図1: 塞がれているのは端末どうしの直通。外向きの通信は通ることが多いので、ハブを外に置けば会場の設定に左右されにくくなる

参加者が Claude Code を使えている時点で、少なくとも Anthropic の API への経路は開いている。外向きの通信そのものが全面的に塞がれている会場では、そもそもセミナーが成立しない。だからハブを会場の中ではなく外に置けば、条件はぐっと緩くなる。

ただし「外向きなら何でも通る」とまでは言えない。特定のドメインだけを許可する会場もあるので、当日そのURLへ到達できるかは事前に確かめておく必要がある。ブラウザでハブのURLを開いて応答が返れば足りる。

置き場所は Cloudflare Workers あたりが手頃になる。常時起動しているサーバーを持たなくてよく、URL が固定される。会場が変わっても、配る1行は毎回同じものが使える。当日ホワイトボードにIPアドレスを書く必要もない。

進捗が集まる仕組み

Claude Code にはフックがある。「セッションが始まった」「ユーザーが入力した」「Claude が応答を終えた」といった節目でシェルコマンドを走らせる仕組みで、設定ファイルに書いておくと自動で動く。

使うのは主に StopClaude が応答を終えるたびに発火するフックだ。

参加者のClaude Codeが応答を終えるたびに現況をハブへ送り、講師のダッシュボードがそれを取りに行く流れ
図2: 進捗は聞きに行くのではなく、応答が終わるたびに勝手に集まる。参加者が操作する場面はない

フックにはJSONが標準入力で渡ってくる。Stop なら session_idcwd(作業フォルダ)、last_assistant_message(Claude が最後に何と言ったか)などが入っている。そこから必要な分だけ抜いて送る。

{
  "hooks": {
    "Stop": [{ "matcher": "", "hooks": [{
      "type": "command",
      "command": "~/.claude/seminar-report.sh"
    }]}]
  }
}
#!/bin/bash
# ~/.claude/seminar-report.sh — 現況をハブへ送る
INPUT=$(cat)
TOKEN=$(cat ~/.claude/seminar-token 2>/dev/null) || exit 0
SESSION=$(printf '%s' "$INPUT" | jq -r '.session_id // "unknown"')

# 送る中身は jq で組み立てる(名前やパスに引用符・改行が入っても壊れない)
BODY=$(jq -n --arg s "$SESSION" --arg p "$(cat ~/.claude/seminar-project 2>/dev/null)" \
        '{session:$s, project:$p}')

curl -s -m 3 --fail-with-body -X POST "https://hub.example.workers.dev/api/report" \
  -H "content-type: application/json" \
  -H "authorization: Bearer $TOKEN" \
  -d "$BODY" > /dev/null 2>&1
exit 0

タイムアウトを短くして、必ず exit 0 で抜けるのが大事なところだ。ハブが落ちていても参加者の作業は止まらない。進捗把握のための道具が、参加者の手を止める理由になってはいけない。

echo "{\"name\":\"$NAME\"}" のような文字列連結でJSONを組むと、名前やパスに引用符や改行が混じった瞬間に壊れる。JSONは jq -n --arg で作る

講師側はブラウザで一覧を開いておく。数秒ごとにハブへ取りに行き、名前・作業中のプロジェクト・最終更新時刻を並べる。

指示が届く仕組み

面白いのはこちらだ。フックは、応答を終えようとしている Claude を引き止めることができる

講師が送った指示がハブを経由して参加者のClaude Codeに届き、そのまま実行される流れ
図3: 講師の指示は、相手の Claude が一区切りついた瞬間に差し込まれる。参加者は何も操作しない

Stop フックで終了コード2を返し、標準エラー出力にメッセージを書くと、Claude は応答を終えずにその内容を受け取って続きを始める。これが最も素直な方法になる。

MSG=$(curl -s -m 3 --fail-with-body \
        -H "authorization: Bearer $TOKEN" \
        "https://hub.example.workers.dev/api/inbox") || exit 0

if [ -n "$MSG" ]; then
  echo "講師より: $MSG" >&2
  exit 2          # 応答を終わらせず、この内容を受け取らせる
fi
exit 0

参加者から見ると、自分の Claude が勝手に次の話を始める。席に行ってキーボードを借りる必要がなくなる。

JSON を返して decisionreason で同じことをする書き方もあるが、フィールドをどの階層に置くかが資料によって食い違っている。実装する前に、使っている版の公式ドキュメントで形を確かめたほうがいい。終了コード2はどの資料でも一致しているので、まずはこちらで作るのが安全になる。

ここには落とし穴が3つある

1つ目。未読が無いときに引き止めてはいけない。 毎回引き止めると、応答がいつまでも終わらなくなる。引き止めるのは受信箱に未読があるときだけにする。

2つ目。渡した指示は消す。 受信箱に残したままだと、次に応答が終わったときにまた同じ指示が届き、同じことを何度も実行し続ける。

3つ目。連続で引き止めた回数を数えて、上限で打ち切る。 1つ目と2つ目を実装しても、ハブ側の不具合で同じ指示が返り続ければ同じことになる。ローカルにカウンタを持ち、たとえば3回連続で引き止めたらその後は exit 0 に倒す。相手のセッションを止められるのはこちらではなく相手なのだから、暴走の芽は送り手側ではなく受け手側で摘む。

届くのは「次の切れ目」であって、いますぐではない

Stop フックは応答が終わるときにしか発火しない。参加者の Claude が何もしていない(入力待ちの)間は、指示が届かない。手が完全に止まっている人ほど届きにくい、という素直でない性質がある。

UserPromptSubmit(参加者が何か打ち込んだとき)のフックを併用すると、次に喋りかけた瞬間には届く。ただしこちらで入力を止める側の指定を流用してはいけない。参加者が打った内容そのものが処理されずに消えてしまう。指示を添えるだけなら additionalContext を使う。

{"hookSpecificOutput": {
  "hookEventName": "UserPromptSubmit",
  "additionalContext": "講師より: Step 3 に進んでください。"
}}

それでもリアルタイムの割り込みにはならない。この仕組みは「呼び出し」ではなく「伝言」だと考えたほうが実態に合う。

誰が誰かを決める

送られてきた報告が誰のものかは、こちらで決めておく必要がある。3通りある。

やり方参加者の手間弱点
セットアップ時に名前を聞く1行貼る、名前を打つ特になし
講師の画面で番号に名前を付ける1行貼るだけ「3番は誰か」を1回確認する必要がある
参加者の Claude に名乗らせる会話するだけ全員が同時にやると詰まる人が出る

現実的なのは1つ目で、配るのは全員同じ1行にする。実行すると名前を聞かれ、参加者ごとに違うトークンが発行されて端末に保存される。以降の報告にはそのトークンが付く。

3つ目も成立はする。名前が未登録のとき、SessionStart フックで次の内容をコンテキストに載せておけばいい。

セミナー用の登録がまだです。最初にユーザーへ名前を尋ね、`seminar-register <名前>` を実行してください。

すると参加者が最初に何か話しかけた時点で、その人の Claude が自分から名前を聞いてきて、自分で登録する。ただしセッションが始まった瞬間に Claude が喋り出すわけではない。参加者が一言打ち込むまでは何も起きない。動きとしては面白いが、冒頭で20人が同時にやると事故が起きやすい。

宛先はセッション単位にする

同じ人がウィンドウを2つ開けば、session_id の違う2つとして届く。ここで受信箱を人単位にすると、最初に見に来たウィンドウが指示を持っていってしまう。別のフォルダで作業している側に届いて、意味の通らない指示になる。

受信箱は 参加者 + セッション 単位で持ち、「その人の全ウィンドウに送る」は別の操作として用意する。一覧の表示も「人 / ウィンドウ / 作業中のプロジェクト」まで分けておくと混乱しない。

そのまま作ると危ない場所

ここが設計の本題かもしれない。指示を送るということは、相手の Claude に何かをさせられるということで、覗き見の問題では済まない。

認証がないと、URLを知っている人が誰にでもなりすませる。 名前だけで受信箱を引ける作りにすると、他人の指示を横取りできるし、講師のふりをして任意の指示を送り込める。参加者ごとのランダムなトークン、講師用の別の権限、有効期限と失効、送信回数の制限。ここは省略できない。

「取り出したら消す」は保存先を選ぶ。 Cloudflare Workers の KV は結果整合性で、書き込みが他の拠点から見えるまで時間がかかることがある。読んでから消すまでを一体にできないので、同時に取りに来ると二重に届いたり消えたりする。受信箱には Durable Objects か D1 のような、順序と原子性を担保できるものを使う。メッセージに識別子を持たせ、「渡した」「実行された」を別に管理する。

cwd は思ったより喋る。 フルパスにはユーザー名が入るし、案件名や顧客名がフォルダ名になっていることも多い。会話を送らなければ十分、とはならない。送るのはセミナー用に登録したプロジェクト名だけにして、パスそのものは送らない。

設定を書き込む場所を間違えると、セミナーが終わっても送り続ける。 ユーザー全体の設定に書くと、翌日の別の仕事のフォルダでもフックが発火する。プロジェクト単位の設定に閉じ込め、終了時にフック・トークン・スクリプトを消すところまでを配布物に含める

curl ... | bash は参加者の権限で任意のコードを走らせる。 中身を表示してから実行させる、版を固定する、アンインストール手順を同じ紙に書く。ここを雑にすると、講師が「よく分からないものを実行させる人」になる。

講師の画面もエスケープする。 名前や作業内容をそのまま描画すると、報告APIを通してスクリプトを流し込まれる。

ハブを畳んでもデータは残る。 保存期間と自動削除を最初に決めて、参加者に伝えておく。

何を送らないかを先に決める

Stop フックには last_assistant_message が渡ってくる。その人の Claude が喋った内容が丸ごと入っている。何も考えずに全部送る作りにすると、参加者が自分の仕事の相談を打ち込んだ内容まで、講師の画面に流れることになる。

進捗把握の道具として作るなら、単純に不要な情報だ。送るのは次の3つで足りる。

  • 名前(登録時に本人が入力したもの)
  • セミナー用に登録したプロジェクト名
  • 最後に動いた時刻

そのうえで運用としては、始める前に何を送るかを説明して同意を取り、参加者が1コマンドで止められるようにし、セミナーが終わったら配布物ごと撤去する。

作るとしたら何が要るか

部品中身
ハブCloudflare Workers。受信箱は Durable Objects か D1。報告の受け口・受信箱・一覧API・トークン発行
講師の画面一覧と、宛先を選んでメッセージを送る欄。表示は必ずエスケープする
配布物フックを書き込むセットアップと、撤去用のアンインストール

土台は前の記事で書いた 90行のサーバーがそのまま使える。ただし認証と受信箱の作りだけは、規模のわりに手を抜けない

分かっている限界

  • リアルタイムではない。指示は相手の Claude が一区切りついたときに届く
  • 「更新が止まっている=詰まっている」とは限らない。入力待ち、離席、長い処理、通信断が同じ見え方になる。一覧に出せるのは「要確認」までで、詰まっているかどうかは声をかけて確かめることになる
  • Claude Code のフック仕様に依存する。仕様が変われば追随が必要になる
  • 参加者のPCを直接操作できるわけではない。あくまで相手の Claude に伝言するだけで、実行するかどうかは相手の Claude とその人の判断になる

最後の点はむしろ利点だと思っている。遠隔操作ではなく、伝言である。参加者の画面の主導権は参加者の側に残る。

まとめ

  • 会場の Wi-Fi が端末どうしを遮断していても、ハブを外に置けば成立する。ただしそのURLに到達できるかは当日確かめる
  • 進捗は Stop フックで応答が終わるたびに自動で集まる。参加者は操作しない
  • 指示は同じ Stop フックで終了コード2を返せば相手の Claude に差し込める。未読が無いときは引き止めない・渡したら消す・連続回数を数えて打ち切る、の3点を外すと壊れる
  • 宛先は人ではなくセッション単位にしないと、別のウィンドウが指示を持っていく
  • 認証と受信箱の作りは省略できない。指示を送れる口は、そのまま他人の Claude を動かせる口になる
  • 送るのは名前とプロジェクト名と時刻まで。会話の中身も、フルパスも送らない

作るのはこれからで、まず会場のURL到達性を確かめるところから始める。


この記事の設計は Codex(gpt-5.6-sol)のレビューを1回受け、フックの返し方・受信箱の整合性・認証の欠落・cwd の扱いについての指摘を反映している。