メインコンテンツへスキップ
kt-tech.blog

【設計】Remote Control はアカウントで止まる、herdr は PTY 層で越える — 会社用 Claude の許可待ちを個人の携帯から捌く

設計12分で読めます

この記事でわかること

  • Remote Control と ListAgents がアカウント単位で分離していて越えられないこと(実測で49件中0件)
  • herdr は PTY 層なのでアカウントを問わず全セッションの許可待ちを読めること
  • 個人アカウントの番犬を1つだけ Remote Control 可能にする踏み台構成と、置き場所が構成を決める理由
  • 通知から返信までの数分で許可プロンプトが grep から git push にすり替わった実例と、指紋照合による対策
  • 許可プロンプトの2形式(番号式・カーソル式)と、信頼確認の初期カーソルが No, exit に乗っていること
  • 自動承認をやめた理由と、業務情報が個人アカウントのログに入るという別の論点

Claude Code を複数アカウント(CLAUDE_CONFIG_DIR の切り替え)で使っていること。herdr などのターミナルマルチプレクサでセッションを並べていると、そのまま試せます。

概要: Claude Code を個人用と会社用の2アカウントで使っていて、会社側が許可待ちで止まっても携帯からは気づけないのが地味に不便でした。Remote Control はアカウントに紐づくので越えられません。でも herdr は PTY 層で動くのでアカウントを問わない。この非対称性を使って、個人アカウントの「番犬」を1匹だけ Remote Control 可能にして、そいつに herdr を叩かせる構成にしたら通りました。踏んだ罠も含めて記録します。

きっかけ

Claude Code を2アカウントで使い分けています。個人用と、業務委託先の会社用です。ディレクトリで自動的に切り替わるようにしていて、~/.zshrc にこれを置いています。

Bash
_claude_config_switch() {
  case "$PWD" in
    "$HOME/Desktop/Job/<会社>"|"$HOME/Desktop/Job/<会社>"/*)
      export CLAUDE_CONFIG_DIR="$HOME/.<会社>-claude" ;;
    *)
      unset CLAUDE_CONFIG_DIR ;;
  esac
}
autoload -U add-zsh-hook
add-zsh-hook chpwd _claude_config_switch

CLAUDE_CONFIG_DIR を差し替えると設定もトークンもまるごと別になるので、これ自体はうまく動いています。会社のディレクトリに cd すれば会社アカウント、それ以外は個人アカウント。何も考えなくていい。

困るのはここからで、会社側のセッションが許可待ちで止まっても、携帯からは存在ごと見えないんですよね。個人アカウントの Claude アプリにログインしているので当然といえば当然なんですが、席を外している間に会社の作業が止まっていて、戻ってきて気づく、というのを何度かやりました。

で、herdr(ターミナルのワークスペース管理ツール)で全部のセッションを1画面に並べてはいるので、「herdr を個人の Claude に見張らせれば、アカウント関係なく全部見えるんじゃないか」と思ったのが出発点です。

どの層で見ているかで決まる

先に結論から書くと、越えられるかどうかはどの層で見ているかで決まります。

個人↔会社を越えられるか 理由
Remote Control 越えられない アカウントに紐づく
ListAgents / SendMessage 越えられない 設定ディレクトリ単位で分離
herdr 越えられる PTY 層。どのアカウントでログインしていようが無関係

ListAgents については実測しました。手元で49件のピアが返ってきたんですが、会社アカウントで動いている3セッションは1件も出てきません

面白かったのが、もう1社の業務委託のセッションは普通に出てくることです。そちらは zshrc のフック対象外で個人アカウントのまま動いていたからで、つまり分離しているのは会社名ではなく CLAUDE_CONFIG_DIR なんですよね。当たり前といえば当たり前なんですが、実際に一覧を見るまでは「会社かどうか」で分かれている気がしていました。

一方 herdr は、同じマシンで動いているセッションを全部返してきます。

全 8 セッション (会社 3 / 個人 5)  許可待ち 1 件
────────────────────────────────────────
■ w8:p1  [会社]  <業務プロジェクト>
   種別: bash_permission_prompt
   要求: Bash command
        git push ...
   選択: [1] Yes  [2] No

個人アカウントのセッションから、会社アカウントのセッションが何の許可を待っているかまで読めています。 herdr はターミナルのペインを見ているだけで、その中で動いているのが誰のアカウントの Claude かは知らないし気にしない。だから越えられる。

踏み台構成にする

というわけで、Remote Control が越えられないなら越える必要がないところまで話を持っていけばいいという発想にしました。

個人アカウント 会社アカウント 📱 スマホ claude.ai/code 会社アカウントのセッション Claude Code が複数走っている Remote Control アカウント境界を越えられない Remote Control(1つだけ開ける) 番犬セッション 個人アカウントで常駐させておく herdr(ターミナルマルチプレクサ) PTY 層で動く = アカウントの区別を持たない herdr を叩く 境界を越えて 全セッションに届く
Remote Control はアカウントの内側までしか届かない。herdr は PTY を掴んでいるだけでアカウントを知らないので、個人アカウントで番犬セッションを1つ開けておけば、そこから境界の向こうのセッションまで手が伸びる。

個人アカウントの「番犬」セッションを1つだけ立てて、そいつを Remote Control 可能にしておく。携帯からはその番犬にだけ話しかける。番犬は herdr 経由で全セッションに手を伸ばす。会社アカウントを Remote Control する必要がなくなります。

置き場所がそのまま構成になる

番犬を置くディレクトリは ~/Desktop/private/herdr-watchtower にしました。ここ、地味に効いています。

さっきの zshrc のフックは会社のパス配下でだけ CLAUDE_CONFIG_DIR を差し替えるので、private 配下に置けば番犬は必ず個人アカウントで起動します。実際にペインを作って確認しました。

$ echo "CFGDIR=[${CLAUDE_CONFIG_DIR:-unset}]"
CFGDIR=[unset]

逆に言うと、もし会社のディレクトリ配下に番犬を作っていたら、番犬自身が会社アカウントになって Remote Control できず、構成ごと成立しなかったわけです。置き場所を決めた時点で勝ち負けが決まっていたやつで、これは偶然でした笑

使っている herdr のコマンド

道具はこれだけです。

コマンド 用途
herdr agent list 全セッションの状態。blocked が許可待ち
herdr agent explain なぜ止まったか(検出ルール名で返る)
herdr pane read 画面を読む
herdr agent send-keys Yes/No を送る
herdr agent prompt 普通のプロンプトを送る

explain が思ったより便利で、止まった理由を検出ルール名で返してくれます。bash_permission_prompt なのか mcp_elicitation_prompt なのか live_blocked_form なのかが分かるので、通知の文面を出し分けられる。

そして Yes/No だけじゃなく herdr agent prompt で普通の指示も送れます。 ここに気づいてから、携帯が「承認ボタン」ではなく「全セッションのコンソール」になりました。「テストだけ流して報告して」とか「一旦止めて」とか、そのまま投げられます。

罠1: 許可プロンプトはすり替わる

ここが一番危なかったところです。

作っている最中、会社セッションが grep の許可待ちで止まっているのを観測しました。読み取りだけだし通していいやつです。……で、数分後に同じペインを見たら、同じ位置が git push に変わっていました。

# さっき見たとき
要求: grep -n "..." <ファイル>

# 数分後、同じペインの同じ位置
要求: git push -u origin <ブランチ>

Claude は1件承認すると次の許可プロンプトを即座に出すので、「携帯に通知が飛ぶ」→「それを見て返信する」の間に対象が入れ替わるんですよね。番号だけ送る実装にしていたら、読み取りコマンドを承認したつもりで会社のリポジトリに push していたことになります。

しかもこれ、1回きりの偶然ではなくて、作業中に2回起きました。通知を見て返すまでの数分って、エージェントにとっては十分長いんだなと。

対策として、プロンプトの中身を8桁に畳んだ指紋を通知に含めるようにしました。

Python
def fingerprint(prompt):
    material = "\n".join([prompt["header"], *prompt["body"],
                          *(o["text"] for o in prompt["options"])])
    return hashlib.sha256(material.encode()).hexdigest()[:8]

承認する側は指紋が一致したときだけキーを送ります。

$ python3 approve.py w8:p1 yes --fp deadbeef

中止: 指紋が不一致 (通知時 deadbeef / 現在 ae57f4ee)
プロンプトが入れ替わっている。上の現在の内容を見て、改めて判断すること。
重要

リモートから承認する仕組みを作るなら、これは必須だと思っています。

画面に出ているものと、承認が届くときに出ているものは、別物になりうる。通知に「何を承認しようとしているか」を書くだけでは足りなくて、送る瞬間にもう一度照合する必要があります。

罠2: 許可プロンプトには2つの形式がある

もうひとつ、実装してから気づいたやつです。

Bash の許可プロンプトは番号付きで出ます。

Do you want to proceed?
❯ 1. Yes
  2. No

なので数字キーを送ればいい、と思って書いていたんですが、テスト用に新しいペインで Claude を立ち上げたら、フォルダの信頼確認がこう出ました。

❯ No, exit
  Yes, I trust this folder

Enter to confirm · Esc to cancel

番号がない。 カーソルを矢印で動かして Enter で選ぶ形式です。番号前提のパーサだと選択肢がひとつも拾えず、承認しようとしても「対応する選択肢がない」で止まります。

形式 見た目 送るキー
番号式 ❯ 1. Yes 数字キー
カーソル式 ❯ No, exit 矢印 + Enter

カーソル式を拾うのが少し面倒で、 の行を起点に上下へ「空行でない」かつ「本文の開始桁が同じ」行をたどる、という判定にしました。メニュー項目は桁が揃うけれど説明文は揃わないので、これで分離できます。

ちなみにこの信頼確認、デフォルトのカーソルが No, exit に乗っています。 雑に Enter を送る実装だとセッションが終了する。よくできてるなと思いました。

番犬を常時つないでおく

Remote Control はセッション単位で有効にするものなので、番犬は起動時からつながっていてほしい。/rc を手で押すのは避けたかったので探したら、フラグがありました。

Bash
cd ~/Desktop/private/herdr-watchtower
claude --remote-control watchtower

--remote-control [name] で、Remote Control を有効にした状態で起動できます。名前を付けておくと携帯側の一覧で見分けが付く。herdr のペインの中で起動すればターミナルを閉じてもセッションは生き残るので、これで常駐します。

やらなかったこと: 自動承認

read 系のコマンドだけホワイトリストで自動承認する、というのは最初考えました。通知の数がだいぶ減るので。

やめました。許可プロンプトが出るということは、Claude が自分では判断できないと思った操作だということなので、そこを機械的に通すのは permission mode を bypass にするのとあまり変わらないなと。

実際、今回作業中に止まったやつは grepgit push でした。前者だけ通す設計は書けますが、その2つが同じペインの同じ位置で入れ替わるのを目の前で見た後だと、ホワイトリストの判定が「今まさに表示されているもの」を見ている保証がほしくなります。 それはもう指紋照合と同じ話で、だったら人間が見て決めればいいかなと。

番犬がやるのは通知と要約まで。Yes/No は自分で出します。

情報の流れはまた別の問題

技術的には通ったんですが、運用として引っかかっている点がひとつあります。

番犬が会社セッションの画面を読むと、業務の内容が個人アカウントの会話ログに入ります。 今回も検証中に、会社のリポジトリのファイルパスやブランチ名が個人アカウント側の会話に流れ込みました。会社アカウントはチームプランで、個人は Max。データの扱いが同じとは限らないんですよね。

なので「どこまで読むか」は分けられるようにしてあります。

  • 件数だけ: 「会社セッションが1件止まっています」だけ通知する。中身は取らない
  • 要約まで: 何のコマンドで止まっているかを通知に含める(今の設定)

携帯だけで判断したいなら後者が要るし、内容を渡したくないなら前者にして PC に戻る。ここは技術じゃなくて、どっちを取るかの話だと思っています。

まとめ

  • 越えられるかどうかは、どの層で見ているかで決まる。 Remote Control と ListAgents はアカウント(CLAUDE_CONFIG_DIR)単位で分離していて越えられない。herdr は PTY 層なので越えられる
  • Remote Control が越えられないなら、越える必要がないところまで話を持っていく。 個人アカウントの番犬を1つだけ Remote Control 可能にして、そいつに herdr を叩かせる
  • 番犬の置き場所がそのまま構成を決める。会社のパス配下に置くと番犬自身が会社アカウントになって成立しない
  • 許可プロンプトはすり替わる。 通知を見て返信するまでの数分で grepgit push に化けた。リモート承認するなら指紋照合は必須
  • 許可プロンプトには番号式とカーソル式の2形式がある。信頼確認はカーソル式で、しかも初期カーソルが No, exit に乗っている
  • claude --remote-control <name> で最初からつないだ状態で起動できる
  • 自動承認はやめた。止まっている=判断できなかった操作、というサインを潰すことになるので
  • 技術的に読めることと、読んでいいことは別。 会社セッションの画面を個人アカウントで読むと、業務情報が個人側のログに入る

参考リンク

関連する記事はこちらです。

更新履歴

  1. 公開。