別のPCのClaude Codeに話しかける — ブラウザ操作をやめてLANのHTTPで往復させた

開発claude-code-toolsメモ

2台のPCでClaude Codeを使っていると、片方のセッションからもう片方へ「これやっておいて」と伝えたくなる。ところがこれが、長いあいだ片方向しか通らなかった。MacからWindowsへは送れるのに、WindowsからMacへは送れない。仕方なくブラウザでclaude.aiを開き、セッション一覧から相手を選び、入力欄にJavaScriptで文字を流し込み、スクリーンショットで確認してから送信ボタンを押していた。

これをコマンド1回にした。同じWi-Fiの中でHTTPを1往復させるだけで、返信まで受け取れる。

send-to-mac.sh --wait-reply "調べておいてほしいことがあります"
→ ── Mac からの返信(MSG-ID: b3a50a6f)──
   こんにちは、Macです。届いています

以下は実際に動かすまでの記録で、素直に作ると危ない場所が中心になる。

なぜ片方向しか通らなかったのか

Claude Codeにはセッション間でメッセージを送る SendMessage という機能がある。Windowsから使うと必ずこう返ってきていた。

Cross-session messaging is not available in this session.

長いこと「Windowsだから使えない」と理解していたが、これは誤りだった。正しくはこうだ。

claude.ai経由で開かれたセッションだけが送信口を持つ。ターミナルから素に起動したセッションは持たない。

Macから送れていたのは、そのセッションをclaude.aiのCode画面から開いていたからにすぎない。実際、今回Mac側にターミナルで新しいセッションを立てたところ、Macからも送れなくなった。機体の問題ではなく、起動経路の問題だった。

素直な解法とその落とし穴

同じWi-Fiにいるのだから、相手のPCで小さなHTTPサーバーを動かし、そこへPOSTすればいい。実際、別の記事で書いた録音データの受け渡しはそれで足りている。

問題は受け取ったメッセージをどうやってClaude Codeに渡すかだ。相手のClaude Codeはターミナルの中で動いている。外から文字を届ける取っ手が要る。

そこで tmux を使う。ターミナルを多重化するツールで、外部から次の4つができる。

  • 文字を流し込む(paste-buffer
  • 画面を読む(capture-pane
  • 前景で何が動いているか調べる(display-message
  • Enterだけを別に送る(send-keys
WindowsからMacへHTTP POSTし、Mac上のサーバーがtmuxのペインへ注入する流れ
図1: ブラウザのDOM操作をやめると、経路は「HTTP 1回 + tmuxへの注入」だけになる

ここまでは単純だ。危ないのはこの先である。

tmuxへの注入は「メッセージ配送」ではない

設計を外部レビュー(Codex)に出したところ、最初の版は実装不可と判定された。指摘はこうだった。

tmuxへのキー注入は「メッセージ配送」ではなく、その瞬間の前景プロセスへの無条件入力である。

つまりこういうことだ。

  • 相手のペインがシェルに落ちていれば、送った本文がコマンドとして実行される
  • 権限確認ダイアログが開いていれば、Enterがダイアログの承認として消費される

「メッセージを送る」つもりの操作が、相手のマシンで任意コマンドを実行する操作になりうる。これは覗き見の問題では済まない。

3段構えで防ぐ

そこで注入の前に確認を挟んだ。

1つ目。前景プロセスを確認する。 Claude Codeが動いているペインでなければ、何もせず409を返す。シェルへの注入を構造的に遮断する。

ここで実測が計画を裏切った。pane_current_command の値は node でも claude でもなく、2.1.234 というバージョン番号そのものだったのだ。実行ファイルがバージョン名のディレクトリへのシンボリックリンクになっているため、その名前がそのまま出る。計画どおり「nodeclaude以外は拒否」と書いていたら、一通も届かなかった

2つ目。アイドル入力待ちのときだけ許可する。 応答生成中・ダイアログ表示中・入力欄に打ちかけの文字がある状態は、すべて拒否する。

当初は「応答生成中でも入力欄に入れておけば、ターン終了後に処理される」と考えていたが、これもレビューで否決された。確認した直後に画面が切り替われば、Enterが別のUIに入るからだ。確認と送信は原子的ではない。アイドルに限れば、画面が勝手に変わる要因は本人の操作だけになる。

3つ目。2段階で注入する。 まず本文だけ貼り付け、画面を読んで届いていることを確かめてから、Enterを別に送る。確認できなければEnterを送らない。Enterさえ送らなければ、コマンド実行もダイアログ確定も起きない。

安全確認を通ったときだけ注入し、危険な状態では409を返して送信側がリトライするシーケンス
図2: 拒否は失敗ではなく正常動作。何も注入していないので、送信側が安心して再送できる

拒否されたときは送信側が15秒おきに再送する。実際、テスト中に相手が応答生成中で3回連続で拒否され、4回目で通った。

待機中(1/5): busy: 応答生成中の表示を検出: "(11s ·"
待機中(2/5): busy: 応答生成中の表示を検出: "(27s ·"
待機中(3/5): busy: 応答生成中の表示を検出: "(42s ·"
送信成功(192.168.10.102:7871)

経過時間が伸びているのが見える。相手が本当に働いていることがログから分かる。

リトライしてよい失敗と、してはいけない失敗

ここは分けておかないと事故になる。

状態何が起きているか再送
409安全確認で弾いた。何も注入していないしてよい
500貼り付け済みだが確認が取れずEnterを送らなかったしてはいけない(入力欄に本文が残っており、重複する)

500のときは自動で再送せず、画面の状態を添えて報告し、人が片付ける。

返信をどう返すか — 対称にしない

送れるようになったが、返事が返ってこなかった。受け口はターミナルで素に起動したセッションなので、SendMessage の送信口を持っていない。冒頭の話がそのまま逆向きに再現された。

素直に考えれば、Windows側にも同じサーバーを立てればいい。だが調べると、Windows側には tmuxscreen も無く、WSLもディストリビューションが未導入だった。導入すれば普段の起動方法が変わるし、疑似端末を自作するのは重い。

そこで発想を変えた

返信が要るのは、こちらが送って待っているときだけではないか。

待っているなら、押し込んでもらう必要はない。取りに行けばいい。

送信時に発行されたIDで返信を長ポーリングし、相手が返信を投函すると待っているコマンドの出力として届く流れ
図3: 返信はキー入力ではなく「待っているコマンドの出力」として届く。だから注入の安全確認がまるごと要らない

送信時にメッセージIDが発行され、それが本文の署名にも埋め込まれる。相手はそのIDを使って返信を投函し、こちらは同じIDで待っている。返信はキーストロークではなく標準出力として届くので、さんざん苦労した注入の安全確認が、この経路には一切要らない

返信箱は1時間保持されるので、待ちが切れても後から回収できる。

割り切った点も書いておく。相手が自発的に(こちらが待っていないときに)送ってくる経路は作っていない。2台運用の実態では「依頼して待つ」が支配的なので、まずこれで足りる。

誰に届くのか — tmuxの外は見えるが送れない

動くようになってから、相手のマシンに何個Claude Codeが動いているか数えてもらった。

tmux内: 1個  ← ブリッジで宛先にできるのはこれだけ
tmux外: 3個  (ターミナル直起動2個、デスクトップアプリ経由1個)
合計:  4個

存在は全部見えるが、送れるのはtmuxの中の1個だけである。プロセス一覧には全部出るが、外から文字を流し込む取っ手を持っているのはtmuxのペインだけだからだ。

これは制約に見えるが、受信口が1つに固定されるという意味では都合がいい。宛先を取り違えて無関係な作業に割り込む事故が起きない。

なお、サーバー自身が動いているtmuxペインも一覧には出るが、そこには入力欄が無いので拒否される。安全確認が効いている証拠でもある。

分かっている限界

  • 相手がアイドルのときしか届かない。作業中は最大5回まで待って再送し、それでも通らなければ諦めて報告する
  • 200は「入力欄に入れてEnterを送った」ことしか保証しない。意図どおり処理されたかは返信で確かめる
  • Claude CodeのTUIの見た目に依存している。入力欄や応答中表示の描画が変われば判定が壊れる。回帰テストは置いたが、バージョンが上がったらまずここを疑う
  • 同じLANにいることが前提。相手がスリープ中や外出先なら、従来のブラウザ経路に戻る

やめるときのために

この手のものは、作ったあとで「面倒だからやめた」となることがある。だから撤去手順を最初から計画書に書いた

仕組み全体が「設定リポジトリの2箇所 + トークン3箇所 + tmuxセッション2つ」に閉じている。外部サービスへの登録も、ルーターのポート開放も無い。手順を上から実行すれば痕跡なく巻き戻り、連絡手段は自動的に従来のブラウザ経路に戻る。

戻し方が書いてあると、思い切って入れられる。

まとめ

  • SendMessage が使えるかどうかは機体ではなく起動経路で決まる。claude.ai経由で開いたセッションだけが送信口を持つ
  • LAN内のHTTPで届けるなら、tmuxが唯一のまともな取っ手になる。ただし注入は「前景プロセスへの無条件入力」であって、配送ではない
  • 安全確認は前景プロセス・アイドル判定・2段階注入の3段構え。拒否(409)は正常動作で、送信側が再送する
  • 貼り付け後に失敗したときだけは再送してはいけない。重複する
  • 返信は対称に作らず、待っている側が取りに行くと、注入の危険がまるごと消える
  • 実装前の外部レビューで2回否決された。素直に作っていたら、相手のマシンでコマンドを実行する道具になっていた

この設計はCodex(gpt-5.6-sol)のレビューを4回受け、tmux直注入の危険性・確認と送信の競合・リトライの分類について指摘を反映している。