handoff について
handoff は、対応する CLI のコーディングセッションを Lark に引き継ぎ、端末を離れても Claude との対話を続けられるスキルです。設定、セッション引き継ぎ、グループ管理、診断、プロファイル、エージェント操作に対応します。
主な機能
- 01セッションを Lark のグループに引き継ぎ
- 02引き継ぎ設定のセットアップと確認
- 03グループとプロファイルの管理
- 04権限ブリッジの往復通信を診断
- 05インストール済みコンポーネントの更新や削除
- 06macOS 上のインストール済みエージェントを操作
活用シーン
- 01Lark 経由で CLI のコーディングセッションを継続
- 02引き継ぎ開始前に設定を確認
- 03sidecar モードで外部 Lark グループに参加
- 04インストール済みエージェントの確認や管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ verneagent/ handoff --skill handoffインストール後、Claude Code で/handoffを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.gitignoreCLAUDE.mdSKILL-commands.mdSKILL-agent.mdREADME.mdhooks.jsonSKILL-setup.mdassets/opencode/plugins/handoff.tsassets/opencode/scripts/permission_bridge.pyhandoff_tool_forwarding.jsscripts/handoff_lifecycle.pyenter_handoff.pygroup_config.pyhagenthandoff_db.pyhandoff_config.pyend_and_cleanup.pyhandoff_worker.pyhandoff_agent.pyhandoff_ops.pyinstall_hooks.pyinstall_agent.shiterm2_silence.pylark_im.pyon_post_tool_use.pyon_pre_compact.pylark_auth.pyon_notification.pyon_pre_tool_use_bash.pyon_session_end.py詳細
CLI セッションを Lark に引き継ぎ、ユーザーが外出先でも Claude とのやり取りを続けられるようにします。
スキルのパス
このドキュメント内のすべてのスクリプトパスは、このスキルのベースディレクトリからの相対パスです。たとえば、scripts/preflight.py は <base_directory>/scripts/preflight.py を意味します。ベースディレクトリは、このスキルを読み込む際にホストツールから提供されます。
プラットフォームの検出
このプロトコルに従う前に、実行環境を特定してください。
- Claude Code —
claudeCLI を使用しています。このドキュメントの記載どおりに進めてください。 - OpenCode —
opencodeエージェントを使用しています。以下の OpenCode の上書き設定を全体に適用してから、このドキュメントの残りの手順に従ってください。 - エージェントモード — Claude Agent SDK を介して Handoff エージェントプロセス内で実行しています(確認方法:
HANDOFF_SESSION_TOOL環境変数がClaude Agent SDKに設定されています)。以下の エージェントモードの上書き設定を適用してください。ハンドオフループに入ったり、待機スクリプトを呼び出したりしないでください。エージェントプロセスがメッセージの入出力を処理します。 - Codex またはその他のツール — 停止してください。 このスキルは Claude Code、OpenCode、エージェントモードのみをサポートしています。ユーザーに「このプラットフォームでは Handoff はサポートされていません。」と伝えてください。
エージェントモードの上書き設定
エージェントモードのみ。
HANDOFF_SESSION_TOOL=Claude Agent SDKが設定されている場合、handoff_agent.pyエージェントプロセス内で実行しています。
エージェントモードでは、SDK クライアントは Lark 対応のコーディングエンジンです。ユーザーのリクエストを処理し、テキスト応答を返します。エージェントプロセス(handoff_agent.py)が、Lark からのメッセージの受信と Lark へのメッセージ送信を含む、すべてのメッセージ入出力を管理します。
SDK クライアントの動作は、このドキュメントではなく SKILL-agent.md で定義されています。このドキュメント(SKILL.md)は、CLI および OpenCode のハンドオフ専用です。
アーキテクチャ
- エージェントプロセス:セッションのライフサイクル、メッセージの受信、コマンドのディスパッチ(/clear、/model など)、Lark への応答送信
- SDK クライアント:コーディングタスク、ファイル操作、Lark のメディア入出力(download-image、send-image、send-file、handoff_ops.py 経由の親ローカル操作)
SDK クライアントが行わないこと
send_to_group.pyを呼び出すこと(エージェントプロセスがテキスト結果を送信します)wait_for_reply.pyまたはsend_and_wait.pyを呼び出すこと- セッション、アクティベーション、ディアクティベーション、タブを管理すること
- この SKILL.md ドキュメントに従うこと
OpenCode の上書き設定
OpenCode ユーザーのみ。 以下のプロトコル全体にこれらのルールを適用してください。Claude Code ユーザーはこのセクションを完全にスキップしてください。
プラグインの依存関係
このスキルには、権限のブリッジング、セッションのライフサイクル、通知の転送、環境変数の注入(HANDOFF_PROJECT_DIR、HANDOFF_SESSION_ID、HANDOFF_SESSION_TOOL)を処理する OpenCode Handoff プラグイン(.opencode/plugins/handoff.ts)が必要です。このプラグインは .opencode/plugins/ から opencode によって自動的に読み込まれます。
プラグインがまだインストールされていない場合は、/handoff init のステップ 4 でインストールされます(以下の init の上書き設定を参照)。
サンドボックス
該当しません。dangerouslyDisableSandbox は使用しないでください。すべての python3/curl/ネットワークコマンドは、サンドボックス用フラグなしで通常どおり実行されます。
サブコマンド
ガード対象リストには init、deinit、clear、delete_admin、purge_admin が含まれます。
プリフライト
preflight.py --tool opencode を実行します(プラグインファイルを確認し、Claude のフックはスキップします)。
/handoff init — ステップ 4 の上書き設定
Claude Code のフック設定の手順をスキップします。代わりに、スキルのアセットから OpenCode プラグインファイルをインストールします。
SKILL=$(python3 -c "import os; p='.claude/skills/handoff'; print(p if os.path.isdir(p) else os.path.expanduser('~/.claude/skills/handoff'))")
mkdir -p .opencode/plugins .opencode/scripts
cp "$SKILL/assets/opencode/plugins/handoff.ts" .opencode/plugins/
cp "$SKILL/assets/opencode/scripts/permission_bridge.py" .opencode/scripts/
cp "$SKILL/assets/opencode/scripts/handoff_tool_forwarding.js" .opencode/scripts/
rm -rf "$SKILL/assets/opencode"
サマリーテーブルでは、「hooks」の代わりに 「OpenCode plugin」 と表示してください。
再起動が必要です: OpenCode は起動時にプラグインを読み込みます。init でプラグインをインストールした後、ユーザーに OpenCode を終了して再度開いてから
/handoffを実行するよう伝えてください。
/handoff deinit の上書き設定
SKILL-commands.md の OpenCode の手順に従ってください(フックではなくプラグインファイルを削除します)。
AskUserQuestion
OpenCode では利用できません。AskUserQuestion の呼び出しはすべて、会話内のプレーンテキストの質問に置き換えてください。スコープに応じて適用します。
- ハンドオフが有効になる前(初期設定ウィザード、グループ選択、ステップ C):CLI の会話内でプレーンテキストとして質問します。
- ハンドオフが有効になった後(メインループ):すべての質問を
send_and_wait.py経由で Lark に送ります。対話型の CLI プロンプトは使用しないでください。
具体的な割り込みケース:
- 待機中の Esc/Ctrl+C:CLI で「ハンドオフを終了しますか?」と直接質問します(ユーザーはターミナルを操作中です)。
- メインループ中の BLOCKED チェック:
send_to_group.py経由で質問し、wait_for_reply.pyで返信を待ちます。
権限の橋渡し
プラグインが permission.asked イベントを介して自動的に処理します。設定が必要なフックはありません。
セッションの継続
プラグインは、アクティブなハンドオフ中の session.idle を処理し、継続プロンプトを自動的に送信します。手動での処理は不要です。
ループのキープアライブポリシー(OpenCode)
アイドル中は完全に無音にしてください。 wait_for_reply.py がタイムアウトした場合:
- 直ちに
wait_for_reply.py --timeout 0を無言で再実行します。メッセージもステータス更新も送信しないでください。 - 更新を送信するのは、状態に意味のある変化があった場合、ユーザーから明示的なリクエストがあった場合、または長時間実行中の作業がある場合(>60s)のみにしてください。
- 禁止事項:アイドル中に「待機中…」「まだいます」などのキープアライブメッセージを送信すること。
- 本当に必要な場合は、最大 1 hour の指数バックオフを使用してください。ただし、無音を優先してください。
対話型プロンプト — モード開始時の確認
- ステップ A(
already_active=true)で、ユーザーがno askをリクエストしていない場合は、続行するか再選択するかを CLI の会話内で質問します。 - ステップ C のデフォルトモードで、すべてのグループが使用中の場合は、CLI の会話内で質問します(引き継ぐか、新規作成するか)。
no-askに黙って切り替えないでください。 - グループの選択肢を提示する際は、ステップ B の出力から直接作成し、選択肢の数が
Nグループと一致することを確認してください。一致しない場合は、ステップ B を再実行してください。
サブコマンド
このスキルは、引数を介したサブコマンドに対応しています。
/handoff(引数なし)— 事前チェックを実行します。失敗した場合は、セットアップウィザードをその場で実行するかどうかを確認します。成功した場合は、Handoff モードに入ります。/handoff <group name>— 事前チェックを実行し、すべてのボットチャットからグループ名で検索します。グループが外部(workspace:タグなし)の場合は、自動的にサイドカーモードに入ります。通常の workspace タグ付きグループの場合は、通常モードに入ります。enter_handoff.pyに--group-name '<GROUP_NAME>'を渡します。応答にはsidecar_mode: true/falseが含まれます。true の場合はサイドカーモードの手順(サイドカーモードのステップ 4)に従い、false の場合は通常の手順(ステップ E)に従います。/handoff help— 説明付きで、サポートされているすべてのサブコマンドを表示します。Handoff モードには入りません。いつでも安全に実行できます。/handoff init— 値がすでに存在する場合でも、すべてのステップを対象とした完全な対話型セットアップウィザードを実行します。各ステップでは、「既存の値を保持」(値がある場合)、「新しい値を指定」、または「新規作成」(該当する場合)を選択できます。CLI 専用 — Handoff モード中は実行できません。/handoff check— 事前チェックを実行し、設定済みの項目と不足している項目のステータスレポートを表示します。Handoff モードには入りません。いつでも安全に実行できます。/handoff chats— 現在のユーザーに関連付けられているすべての Lark チャットグループを一覧表示します。Handoff モードには入りません。いつでも安全に実行できます。/handoff chats_admin— ユーザーに関係なく、ボットが参加しているすべての Lark チャットグループを一覧表示します。Handoff モードには入りません。いつでも安全に実行できます。/handoff status— 現在のプロジェクトのハンドオフステータス(有効/無効、オーナー、チャットグループ、最終アクティビティ)を表示します。Handoff モードには入りません。いつでも安全に実行できます。/handoff delete_admin [group name]— チャットグループを名前で削除します。名前を指定した場合は、一致するグループを検索して削除します(最初に確認します)。名前を指定しない場合は、グループを一覧表示し、削除するグループを選択するようユーザーに求めます。Handoff モードには入りません。CLI 専用 — Handoff モード中は実行できません。/handoff purge_admin— 空のチャットグループ(人間のメンバーがおらず、ボットのみ)をすべて検索して削除します。削除前に対象を一覧表示し、確認を求めます。Handoff モードには入りません。CLI 専用 — Handoff モード中は実行できません。/handoff deinit—/handoff initによってインストールされたすべてのもの(Claude Code のフック、OpenCode のプラグインファイル)を削除し、~/.handoff/config.jsonも削除するかどうかを確認します(デフォルト:いいえ)。最初に確認を求めます。CLI 専用 — Handoff モード中は実行できません。/handoff clear— 現在のプロジェクトのチャットグループとハンドオフデータベースを削除します。最初に確認を求めます。CLI 専用 — Handoff モード中は実行できません。/handoff diag [--mode ws|http|both] [--chat-id ID] [--timeout N]— 権限ブリッジの診断を実行します。ボタン付きのテストカードを送信し、応答をポーリングして、往復通信が機能するかどうかを報告します。デフォルトのモードはwsです。Handoff モードには入りません。いつでも安全に実行できます。/handoff sidecar— サイドカーモードに入ります。既存の外部 Lark グループ(ボットが作成したものではないグループ)に参加し、ボット宛てのメッセージ(@メンション、ボットのメッセージへの返信、またはリアクション/スタンプ)のみに応答します。同じハンドオフループを使用しますが、メッセージをフィルタリングし、グループの変更をスキップします。/handoff upgrade— GitHub から最新バージョンをダウンロードしてインストールします。hooks.jsonが変更されている場合はフックを再インストールします。更新されたファイルを報告します。いつでも安全に実行できます。Handoff モード中にも実行できます。/handoff upgrade --check— インストールせずに、更新が利用可能かどうかを確認します。いつでも安全に実行できます。/handoff use:<profile>— 名前付きの設定プロファイルを使って Handoff モードに入ります(例:/handoff use:work)。preflight.py、enter_handoff.py、すべてのhandoff_ops.py呼び出しに--profile '<PROFILE>'を渡します。グループ名と組み合わせることもできます:/handoff use:work MyGroup。/handoff profile— すべての設定プロファイル(デフォルトと~/.handoff/profiles/内のプロファイル)を一覧表示します。現在のプロファイルとデフォルトのプロファイルを示します。Handoff モードには入りません。いつでも安全に実行できます。/handoff profile show— 現在のプロファイル名と設定ファイルのパスを表示します。Handoff モードには入りません。いつでも安全に実行できます。/handoff profile set-default <name>— デフォルトの設定プロファイルを設定します。~/.handoff/default_profileに書き込みます。Handoff モードには入りません。いつでも安全に実行できます。/handoff init profile:<profile>— デフォルトではなく、名前付きプロファイルのセットアップウィザードを実行します。~/.handoff/profiles/<profile>.jsonを作成します。/handoff agentまたは/handoff agent list— インストール済みのエージェントをすべて一覧表示します。Handoff モードには入りません。macOS のみ。いつでも安全に実行できます。/handoff agent install— 対話形式で、グループを選択し、プロジェクトディレクトリとモデルを選んで、新しい launchd エージェントサービスをインストールします。Handoff モードには入りません。CLI 専用。macOS のみ。/handoff agent status [name]— エージェントのステータスと最近のログを表示します。エージェントが 1 つだけの場合、名前は省略できます。Handoff モードには入りません。いつでも安全に実行できます。/handoff agent stop <name>— 実行中のエージェントを停止します。Handoff モードには入りません。いつでも安全に実行できます。/handoff agent start <name>— 停止中のエージェントを起動します。Handoff モードには入りません。いつでも安全に実行できます。/handoff agent uninstall <name>— エージェントを停止して削除します。Handoff モードには入りません。CLI 専用。/handoff agent log [name]— エージェントの最近のログを表示します。Handoff モードには入りません。いつでも安全に実行できます。 引数文字列を解析して、実行するサブコマンドを特定します。
プロファイルの構文: 引数文字列に use:<name> が含まれている場合、プロファイル名を抽出し、すべての Python スクリプトの呼び出しに --profile '<name>' を渡します。他のサブコマンドとの照合を行う前に、引数文字列から use:<name> トークンを削除します。たとえば、/handoff use:work MyGroup の場合、プロファイルは work、グループ名は MyGroup です。profile:<name> が指定されている場合(init 用)も、同様に抽出します。
ガード: init、deinit、clear、delete_admin、または purge_admin を実行する前に、handoff が現在有効かどうかを確認します。有効な場合は実行を拒否し、ユーザーに「Cannot run this command during handoff mode. Send handback first to return to CLI.」と伝えます。
サブコマンドの実装: chats、chats_admin、status、delete_admin、purge_admin、deinit、clear、diag、profile については、同じディレクトリにある SKILL-commands.md を読み、詳しいコードと手順を確認します。
サンドボックス:重要
curl を使用する Bash コマンド、および curl を使用する Python スクリプト(preflight.py、wait_for_reply.py、send_to_group.py、lark_im.py)を呼び出す Bash コマンドは、すべて dangerouslyDisableSandbox: true を指定して実行する必要があります。 サンドボックスが Lark API と Cloudflare Worker へのネットワークアクセスをブロックするため、エラーが通知されずに失敗します。これは、コンテキストの圧縮後も含め、handoff 全体を通じたすべての呼び出しに適用されます。
また、バックスラッシュによる感嘆符のエスケープ問題を避けるため、Bash でメッセージ文字列を指定するときは、必ずシングルクォート(ダブルクォートではなく)を使用してください。
圧縮後の復旧
自動圧縮後、SKILL.md 全体が要約されて失われる場合があります。次の重要事項は必ず保持してください。
- ループ(絶対に終了しない):
wait_for_reply.py(最初のメッセージのみ)→ 処理 →send_and_wait.py(応答を送信し、次のメッセージを待機)→ 処理 →send_and_wait.py→ …。ループが終了するのは「handback」または takeover の場合のみです。 - サンドボックス: ネットワークを使用する Python スクリプトは、すべて
dangerouslyDisableSandbox: trueが必要です。 - AskUserQuestion の禁止: 質問は
send_and_wait.pyを通じて Lark に送信し、AskUserQuestion は決して使用しないでください。 - タイムアウトの処理: スクリプトのデフォルトは**GPT モデルでは 540s、それ以外では 0(無制限)**で、
{"timeout": true}を出力します。タイムアウト時にステータスメッセージを送信してはいけません。 直ちに同じスクリプトを無言で再実行してください。send_and_wait.pyがタイムアウトした場合、メッセージはすでに送信されています。待機を再開するにはwait_for_reply.pyを呼び出してください。 - handback: ユーザーが「handback」と送信した場合、正常に終了します(さようならのメッセージを送信 → 無効化 → 通知を復元)。
- Takeover: ポーリング結果に
"takeover": trueが含まれている場合、別のセッションがこのチャットを確保しています。無言で終了し、handoff_ops.py deactivate、handoff_ops.py tabs-end --session-model '${session_model}'、iterm2_silence.py offを実行します。Lark メッセージは送信しないでください(新しいセッションが対応します)。ローカルに「Handoff taken over by another session.」と出力してください。 - 組み込みコマンド: メッセージを処理する前に、次のコマンドを確認してください。
filter verbose/important/concise(メッセージフィルターを設定)、autoapprove on/off(自動承認を切り替え —handoff_ops.py set-autoapprove <on|off>を実行)、handback(終了)。これらは handoff 内部のコマンドであり、実行するタスクではありません。
handoff が有効な状態でループから外れている場合は、このファイルを再度読み込み、Main Loop セクションから再開してください。
ワークスペース ID とデータ
ワークスペース ID は {machine}-{folder} です。folder は HANDOFF_PROJECT_DIR(設定されていない場合は cwd)から導出されます。例:MacBookPro-Users-alice-projects-myapp。lark_im.get_workspace_id() によって計算され、物理的なコードの場所(マシンとフォルダーパス)を識別します。また、Lark グループの説明に workspace:{id} として保存されます。
Handoff データは、単一の SQLite データベース ~/.handoff/projects/{project}/handoff-data.db に保存されます。データベースでは、安全な同時アクセス(フックとメインプロセス)のために WAL モードを使用します。次の内容が含まれます。
sessionsテーブル — セッションごとの Handoff 状態:session_id(主キー)、chat_id(一意)、session_tool、session_model、last_checked、activated_at、operator_open_id(有効化時に設定のメールアドレスから解決されます。オペレーターのメッセージだけに絞り込みます)、bot_open_id(有効化時にボット情報から解決されます。サイドカーモードのやり取りの絞り込みに使用されます)、sidecar_mode(サイドカーモードの場合は 1、それ以外は 0。スクリプトは CLI フラグを必要とせず、セッションからこの値を読み取ります)、config_profile(有効化に使用した設定プロファイルの名前。フックはセッションからこの値を読み取り、適切な認証情報を読み込みます)。messagesテーブル — 送受信両方向(direction=sent|received)のメッセージ履歴。message_id、source_message_id、chat_id、message_time、text、title、sent_atが含まれます。
ヘルプ(/handoff help)
サポートされているすべてのサブコマンドを、書式付きの表で表示します。Handoff モードには入らないでください。
| コマンド | 説明 |
|---|---|
/handoff | Handoff モードに入ります(事前チェックとガイド付きセットアップを実行) |
/handoff <group name> | 特定のグループに参加します(サイドカーモードか通常モードかを自動検出) |
/handoff help | このヘルプを表示します |
/handoff init | 対話形式のセットアップウィザードを実行します(CLI のみ) |
/handoff check | 事前チェックを実行し、ステータスレポートを表示します |
/handoff chats | 自分の Lark Handoff グループを一覧表示します |
/handoff chats_admin | すべての Lark Handoff グループを一覧表示します |
/handoff status | 現在のプロジェクトの Handoff ステータスを表示します |
/handoff delete_admin [name] | Handoff グループを削除します(CLI のみ) |
/handoff purge_admin | 人間のメンバーがいない空のグループを削除します(CLI のみ) |
/handoff deinit | インストール済みのフック/プラグインファイルを削除します。設定も削除するかどうかを選択できます(CLI のみ) |
/handoff clear | 現在のプロジェクトのチャットグループとデータベースを削除します(CLI のみ) |
/handoff sidecar | サイドカーモード:外部グループに参加し、@メンションにのみ応答します |
/handoff diag | 権限ブリッジの診断を実行します(カード操作からポーリングまでの往復をテスト) |
/handoff upgrade | GitHub から最新バージョンをダウンロードしてインストールします |
/handoff upgrade --check | 更新が利用可能か確認します |
/handoff use:<profile> | 名前付きの設定プロファイルを使用して Handoff に入ります |
/handoff profile | すべての設定プロファイルを一覧表示します |
/handoff profile show | 現在のプロファイルの詳細を表示します |
/handoff profile set-default <name> | デフォルトの設定プロファイルを設定します |
/handoff init profile:<profile> | 名前付きプロファイルのセットアップウィザードを実行します |
/handoff agent | インストール済みのエージェントを一覧表示します(macOS) |
/handoff agent install | チャットグループ用の新しいエージェントをインストールします |
/handoff agent status [name] | エージェントのステータスと最近のログを表示します |
/handoff agent stop <name> | 実行中のエージェントを停止します |
/handoff agent start <name> | 停止中のエージェントを起動します |
/handoff agent uninstall <name> | インストール済みのエージェントを削除します |
/handoff agent log [name] | エージェントの最近のログを表示します |
事前チェック
事前チェックを実行して、すべての要件を満たしていることを確認します。
python3 scripts/preflight.py
プロファイルが指定されている場合(use:<profile> 経由)、--profile '<profile>' を渡します。
python3 scripts/preflight.py --profile '<profile>'
/handoff check の場合
詳細レポートを実行して、ここで停止します。Handoff モードには入らず、セットアップも実行しないでください。
python3 scripts/preflight.py --report
出力をユーザーに表示します。ここには、設定済みの値、フックの状態、Handoff データがすべて表示されます。
/handoff(引数なし)の場合
スクリプトがゼロ以外のコードで終了した場合は、ユーザーに次のように伝えます。「Handoff はまだ設定されていません。/handoff init を実行して開始してください。」 今すぐセットアップを実行するか、終了するかを尋ねます。理由を問わず、Handoff モードには移行しないでください。
/handoff init の場合
事前チェックを省略します。SKILL-setup.md を読み、すべての手順についてガイド付きセットアップを無条件で実行します。
サイドカーモード(/handoff sidecar)
サイドカーモードでは、ハンドオフボットが既存の外部 Lark グループに参加し、オペレーター(ハンドオフ設定にメールアドレスが登録されているユーザー)からのボット宛てメッセージにのみ応答します。メッセージがボット宛てとみなされるのは、(1)ボットへの @メンション、(2)ボットが送信したメッセージへの返信、または(3)リアクションやスタンプの場合です。専用グループを作成する通常のハンドオフとは異なり、サイドカーモードはボットが追加された任意のグループで動作します。
ステップ 1:事前チェック
標準の事前チェックを実行します。失敗した場合は、ユーザーに /handoff init を実行するよう伝えます。
ステップ 2:外部グループを検出する
python3 scripts/handoff_ops.py discover-bot
これにより、ボットがメンバーであり、workspace: タグがないグループ(つまり、ハンドオフによって作成されたものではないグループ)が返されます。出力には bot_open_id と open_id が含まれます(セッションテーブルに入力するため、アクティベーション中に使用します)。
JSON 出力を解析します。N をグループ数とします。
- N == 0 の場合: ユーザーに「外部グループが見つかりませんでした。まずボットを Lark グループに追加してから、もう一度お試しください。」と伝えます。
- N == 1 の場合: グループを自動選択します。
- N >= 2 の場合: 参加するグループをユーザーに選んでもらいます。
ステップ 3:アクティベートする
python3 scripts/handoff_ops.py activate --chat-id '<CHAT_ID>' --session-model '${session_model}' --sidecar
--sidecar フラグはセッションテーブルに sidecar_mode=1 を保存し、すべてのスクリプトでボットとのやり取りのフィルタリングが自動的に適用されるようにします。
ステップ 4:サイドカーモードのループに入る
python3 scripts/start_and_wait.py --session-model '${session_model}'
サイドカーモードの動作はすべてセッションテーブルから読み取られます。sidecar_mode(タブ/カードをスキップし、やり取りのフィルタリングを有効化)、bot_open_id(@メンションの照合)、operator_open_id(送信者のフィルタリング)です。--sidecar CLI フラグは不要です。ステップ 3 のアクティベーション中にすべて保存されています。
ステップ 5:メインループ(通常のハンドオフと同じ)
メインループは通常のハンドオフモードと同じですが、オペレーターからのボット宛てメッセージのみを受信する点が異なります。通常どおり send_and_wait.py を使用してください。サイドカーモード、オペレーター、やり取りのフィルタリングはすべて自動的に適用されます(セッションテーブルから読み取られます)。send_and_wait.py に --sidecar フラグは不要です。
すべてのハンドオフコマンドは同じように動作します。「handback」で終了し、長時間のタスクではハートビートを送信するなどです。
Handoff モードへの移行
ステップ A~D:enter_handoff.py
単一のエントリーポイントスクリプトを実行します。環境変数の解決、セッションチェック、グループの検出、自動選択、有効化を一度に処理します。
python3 scripts/enter_handoff.py --session-model '${session_model}'
ユーザーが明示的にこれらのモードをリクエストした場合は、--mode no-ask または --mode new を渡します。
プロファイルが指定されている場合(use:<profile> 経由)は、--profile '<PROFILE>' を渡します。プロファイルはセッション DB に保存されるため、フックは正しい設定を読み込めます。
ユーザーが特定のグループ名を指定した場合(例:/handoff MyGroup)は、--group-name '<GROUP_NAME>' を渡します。すべてのボットチャットからグループを検索し、外部(サイドカー)かワークスペースタグ付き(通常)かを自動検出します。応答で sidecar_mode が true の場合は、サイドカーのループ経路に従います(サイドカーモードのステップ 4 と同様に start_and_wait.py を使用します)。
ステータス値:
"status": "hooks_pending"— フックはインストールされたばかりで、まだ読み込まれていません。直ちに停止してください。 ユーザーに「Claude Code を終了して再起動してから、/handoffを実行してください。」と伝えます。続行しないでください。技術的な詳細を説明しないでください。"status": "restart_required"— セッション環境変数がありません(フックがまだ実行されていません)。直ちに停止してください。 ユーザーに「Claude Code を終了して再起動してから、もう一度/handoffを実行してください。」と伝えます。この問題を回避したり、不足している値を手動で生成したりしないでください。技術的な詳細を説明しないでください。"status": "ready"— 有効化が完了しました。chat_id、session_id、project_dirを取り出します。応答に"rules"フィールドが含まれている場合、これらはグループ固有のルール(グループごとの CLAUDE.md のようなもの)です。ハンドオフセッション全体を通してこれらに従う必要があります。CLAUDE.md の指示と同等の権限があるものとして扱ってください。ステップ E に進みます。"status": "already_active"— このセッションではすでにハンドオフが有効です。- ユーザーが
no ask/autoを指定した場合:現在のチャットを続行します(ステップ E)。 - それ以外の場合:現在のチャットを続行するか、新しいグループを取得するために
--mode newを指定して再実行するか、ユーザーに確認します。
- ユーザーが
"status": "choose"— すべてのグループが使用中です。Claude はユーザーに確認する必要があります。groups配列から選択肢を作成します(各グループ:ラベル = 名前[active]、説明 = chat_id)。- 「新規作成」オプションを追加します。
- 選択後:
- 「新規作成」:
python3 scripts/handoff_ops.py create-group --existing-names-json '<JSON>'を実行してから、activateを実行します。 - 使用中のグループ:テイクオーバーを実行してから、activate は省略します。
テイクオーバーがpython3 scripts/handoff_ops.py takeover --chat-id '<CHAT_ID>' --session-model '${session_model}'ok: falseを返した場合は、enter_handoff.pyを再実行して、もう一度選択します。
- 「新規作成」:
ステップ E:通知を抑制して初期メッセージを送信し、ループに入る
ショートカット:
python3 scripts/start_and_wait.py --session-model '${session_model}'
これにより、ステップ E.1~E.4(通知の抑制 → タブ → ステータスカード →
wait_for_reply.py)が自動的に実行されます。オプション:
--tab-url <url>— ツール/モデルタブに使用する URL を上書きします--skip-silence、--skip-tabs、--skip-card— すでに実行済みの個別ステップをスキップします--timeout、--no-ws、--interval—wait_for_reply.pyに引き渡します
以下の手動手順に切り替えるのは、デバッグの場合に限ります。
- ターミナルの通知を抑制します(iTerm2 のみ)。
python3 scripts/iterm2_silence.py on
- ハンドオフのタブが存在し、正しい順序(メッセージ、次にツール/モデル)になっていることを確認します。
python3 scripts/handoff_ops.py tabs-start --session-model '${session_model}'
- 初期ハンドオフカードを送信します(ツールは環境変数から、モデルは引数から自動解決されます)。
python3 scripts/handoff_ops.py send-status-card start --session-model '${session_model}'
- メインループに入ります(以下を参照)。
メインループ
重要:これは、ユーザーが「handback」と言うか、テイクオーバーが発生しない限り終了しない、無期限のループです。各メッセージの処理後、Bash ツールの呼び出しは次のメッセージを待機するためにブロックされます。常に処理する新しいメッセージを受け取ります。「ハンドオフは有効/アクティブ/準備完了です」と結論づけて停止してはいけません。次に処理するメッセージが必ずあります。
カードのタイトルは send-status-card によって自動的に解決されます。
- 開始:
Handoff from <tool> (<model>) - 終了:
Hand back to <tool>
ツールは HANDOFF_SESSION_TOOL 環境変数から読み込まれ、モデルは --session-model 引数から読み込まれます。
ステップ 1:最初のユーザーメッセージを待つ(初回のみ)
このステップはループ開始時に1 回だけ実行されます。それ以降のメッセージは、ステップ 4 の send_and_wait.py 経由で届きます。
python3 scripts/wait_for_reply.py
- JSON 出力を解析します。スクリプトは Claude モデルの場合は無期限に(timeout=0)、GPT モデルの場合は最大 540 秒間待機し、返信がない場合は
{"timeout": true}を返して正常に終了します。 - ユーザーが割り込んだ場合(Esc または Ctrl+C — Bash ツールの呼び出しが拒否されます):AskUserQuestion を使ってユーザーに「引き継ぎを終了しますか?」と尋ねます(ユーザーが CLI 上にいるため、これがループ中に AskUserQuestion を使用できる唯一の場面です)。確認された場合は、Lark に「Handing back to CLI.」を送信してから、無効化し、通知を復元し、ループを停止して、引き継ぎモードを正常に終了します。拒否された場合は、ステップ 1 に戻ります(
wait_for_reply.pyを再度呼び出します)。 takeoverの場合:別のセッションが引き継いでいます。引き継ぎモードを通知なしで終了します(無効化し、通知を復元します)。ローカルに「Handoff taken over by another session.」と表示します。Lark メッセージは送信しないでください(新しいセッションが対応します)。timeoutの場合:タイムアウトまでに返信がありません。直ちにwait_for_reply.pyを再度呼び出します。これは通常の待機動作です。- 返信が見つかった場合:すべての返信テキストを連結して、ユーザーのメッセージとして扱います。各返信の
mentions配列も確認してください。各 @ メンション対象ユーザーについて、{"key": "@_user_1", "id": "ou_xxx", "name": "Lucy"}が含まれています。テキストフィールドでは、メンションに@_user_1のようなプレースホルダーが使われています。mentions配列を使って実際の名前とopen_idを取得してください。ゲスト/共同所有者のコマンドではこれが重要です。ステップ 2 に進みます。
ステップ 2:コマンドを確認する
フィルターコマンド:返信テキストのいずれかが filter verbose、filter important、filter concise(大文字小文字を区別しない)に一致する場合、メッセージフィルターを更新します。
python3 scripts/handoff_ops.py set-filter <level>
Lark に簡潔な確認メッセージ(例:「Filter set to verbose」)を送り、次のメッセージを send_and_wait.py 経由で待つループに戻ります。
フィルターレベルは、PostToolUse の Lark への転送を制御します。
- verbose — Edit、Write、Bash の出力を転送します
- important — Edit と Write のみ転送します(エラーがない限り Bash は省略します)
- concise — PostToolUse の転送は行いません(デフォルト)
自動承認コマンド:返信テキストのいずれかが autoapprove on、autoapprove off、auto approve on/off、または類似の表現(大文字小文字を区別しない)に一致する場合、自動承認モードを切り替えます。
python3 scripts/handoff_ops.py set-autoapprove <on|off>
Lark に確認メッセージを送ります。自動承認がオンの場合、すべての権限リクエストが承認/拒否カードを送らずに自動承認されます。ユーザーが中断を望まない、信頼できるセッションで役立ちます。オフ(デフォルト)の場合は、通常の権限ブリッジのフローが適用されます。
群规/グループ規範コマンド:グループ規範はキーと値のマップです(グループごとの CLAUDE.md のようなものです)。「群规」、「group norm」、または「gnorm」の場合にのみ反応してください。「rule」、「rules」、「规则」のような一般的な語句だけでは反応しないでください(カーソルルール、inscribe、その他のスキル指示を指している可能性があります)。
-
追加/更新:「群规 lang: 回复用中文」、「gnorm prod: don't touch prod」、「group norm workflow: explain before coding」などです。キー(トリガーの後に続く最初の単語)とテキスト(コロン/スペースの後の内容)を抽出し、次を実行します。
python3 scripts/handoff_ops.py add-norm --key '<KEY>' --text '<RULE_TEXT>'追加/更新したルールを示す確認メッセージを Lark に送ります。
-
削除:「删群规 prod」、「群规删 lang」、「gnorm remove prod」などです。キーを抽出し、次を実行します。
python3 scripts/handoff_ops.py remove-norm --key '<KEY>'Lark に確認メッセージを送ります。
-
すべて表示:キー/テキストを伴わない単独の「群规」、「gnorm」、または「group norms」の場合です。次を実行します。
python3 scripts/handoff_ops.py get-normsルールを整形したリストとして Lark に送信します。ルールが空の場合は「No rules set.」と送信します。
ルールはグループのピン留めされた設定カードに保存され、今後のセッションが有効になるたびに自動的に読み込まれます。
ゲストと共同所有者のコマンド:所有者は、ボットとやり取りできるメンバーの許可リストを管理できます。通常モードとサイドカーモードの両方で機能します。これらのコマンドを柔軟に検出してください(自然言語、任意の言語に対応)。
-
ゲストの追加:オーナーがゲストアクセスを許可する意図でユーザーに言及します。例:「add @jack @alice as guest」、「@jack 和 @alice 可以和你对话」、「let @bob talk to you」。テキストには
@_user_1のようなプレースホルダーが含まれます。返信のmentions配列から実際のopen_idとnameを取得してください(ボット自身へのメンションは除外します)。その後:python3 scripts/handoff_ops.py guest-add --guests-json '[{"open_id":"ou_xxx","name":"Jack"},{"open_id":"ou_yyy","name":"Alice"}]'追加したユーザーを一覧にして、Lark に確認を送信します。
-
共同オーナーの追加:オーナーが共同オーナーアクセスを許可する意図でユーザーに言及します。例:「add @alice as coowner」、「@alice 是 coowner」、「make @bob a coowner」。上記と同様に、返信の
mentions配列からopen_idとnameを取得してください(テキストからは取得しません)。その後:python3 scripts/handoff_ops.py guest-add --role coowner --guests-json '[{"open_id":"ou_xxx","name":"Alice"}]'共同オーナーとして追加したユーザーを一覧にして、Lark に確認を送信します。
-
メンバーの削除:オーナーがアクセス権を取り消す意図でユーザーに言及します。例:「remove @jack」、「@alice 不要了」、「revoke @bob」。メンションから
open_idを取得し、その後:python3 scripts/handoff_ops.py guest-remove --open-ids-json '["ou_xxx"]'削除したユーザーを一覧にして、Lark に確認を送信します。
-
メンバー一覧の表示:オーナーが「guests」、「members」、または「who has access」と言います。次を実行します:
python3 scripts/handoff_ops.py guest-listメンバー一覧を Lark に送信します(各メンバーのロールが表示されます)。
共同オーナーの権限ルール:返信に "privilege": "coowner" が含まれる場合、オーナーと同じように扱います。
- オーナーと同じ権限:シェル、Git、ファイル編集、プロジェクトへのフルアクセス
- 権限リクエスト:共同オーナーは権限カードを承認または拒否できます(オーナーと同じです)。
- オーナーによる上書き:オーナーは共同オーナーの判断を上書きできます。両者が権限カードに応答した場合、オーナーの判断が優先されます。
ゲストの権限ルール:返信に "privilege": "guest" が含まれる場合、低権限のリクエストとして扱います。
- 許可される操作:質問する、説明を受ける、一時フォルダー(
$TMPDIR)内に新しいファイルを作成する、一時ファイルを変更する - 許可されない操作:シェルコマンドの実行、Git 操作、既存のプロジェクトファイルの読み取りや変更、シークレット(.env、認証情報、API キー)へのアクセス、プロジェクトディレクトリ内でのファイル作成
- 権限リクエスト:権限カード(承認/拒否)は、オーナーと共同オーナーからのものだけが処理されます。ゲストのボタンクリックは無視されます。
- オーナーによる上書き:オーナーの指示が常に優先されます。オーナーが「stop」と言った場合、ゲストから依頼された作業はただちに中断します。
- ゲストに返信するときは、
send_and_wait.pyで--mention-user-idを使って @メンションします(「重要事項」の一般的な @メンションの対象指定ルールも参照してください)。python3 scripts/send_and_wait.py '<response>' --mention-user-id '<guest_open_id>'
サイドカーモードと通常モード:サイドカーモードと通常モードの唯一の違いは、ボットとのやり取りのフィルター(filter_bot_interactions)です。サイドカーモードでは、メッセージはボット宛て(@メンション、ボットへの返信、またはリアクション/スタンプ)である必要があります。通常モードでは、許可された送信者からのすべてのメッセージがそのまま処理されます。ゲストと共同オーナーのサポートは、どちらのモードでも同じように機能します。
リレーコマンド:ユーザーが別の引き継ぎグループへの情報共有/リレー/転送を依頼した場合(例:「share this to group X」、「relay to meadow」、「把这个发到 X 群」、「分享到 X」)、意図を検出して次を実行します。
- グループ名で対象グループを検索します(
handoff_ops.py list-groups --scope allの結果とあいまい一致させます)。候補を絞れない場合は、フォームカードで選択肢を提示します。 - リレーを送信します。
これにより、Worker 経由でメッセージを送信し(アクティブなセッションがある場合は対象の DO にプッシュされます)、対象グループにも Lark カードを送信します(アクティブなセッションがない場合でも表示されます)。python3 scripts/handoff_ops.py relay --target-chat-id '<CHAT_ID>' --message '<content>' - ユーザーに「[グループ名] に送信しました」と確認します。
リレーを受信したとき(返信に msg_type: "relay" が含まれる場合)は、ユーザーに提示します。
- 絵文字のインジケーターを使って送信元を表示します:💬
from_chat_name(グループ名が本文に埋もれないようにします) - メッセージの内容を表示します
- ユーザーは返信できます。返信を送信元グループに送り返すには
relayを使います
ハンドバックコマンド:返信テキストに handback または hand back が含まれる場合(大文字小文字は区別しません)、Handoff モードを終了します。テキストに dissolve を含めることもでき(例:「handback dissolve」、「hand back dissolve」)、その場合はハンドオフの終了後にチャットグループも解散(削除)します。 重大:自分から引き継ぎを開始しないでください。 引き継ぎを実行するのは、ユーザーがメッセージとして「handback」または「hand back」と明示的に送信した場合に限ります。(a)タスクが完了したように感じる、(b)会話が途切れているように見える、(c)自分の返信に「handback?」と書いて同意を得たと思い込む、といった理由で引き継ぎを行わないでください。ユーザー自身が言う必要があります。自分のメッセージは該当しません。気軽に引き継ぎを提案することも避けてください。 タスクの完了や会話の中断は、「CLI に引き継ぎますか?」と尋ねる理由にはなりません。CLI に戻ることでユーザーにメリットがある具体的かつ正当な理由がある場合(たとえば、ツールの制限によって進行が本当に妨げられている場合)に限り、引き継ぎに言及してください。セッションを終了する方法はユーザーも知っています。
通常の引き継ぎ(溶解なし):
- 推奨ヘルパー:
これにより、カード → タブ → 無効化 → サイレントの手順が自動的に実行されます。python3 scripts/end_and_cleanup.py --session-model '${session_model}' - 手動手順(ヘルパーを実行できない場合):
- Lark に引き継ぎカードを送信します(セッションがまだアクティブな状態になるよう、無効化する前に実行してください):
python3 scripts/handoff_ops.py send-status-card end --session-model '${session_model}' - この引き継ぎのセッションタブを削除します(ツール/モデル):
python3 scripts/handoff_ops.py tabs-end --session-model '${session_model}' - 引き継ぎを無効化します(ローカル DB からセッションを削除します):
python3 scripts/handoff_ops.py deactivate - ターミナル通知を復元します:
python3 scripts/iterm2_silence.py off - ローカルで「Handoff ended. Back to CLI.」と出力します。
- ループを停止します。
- Lark に引き継ぎカードを送信します(セッションがまだアクティブな状態になるよう、無効化する前に実行してください):
溶解を伴う引き継ぎ(返信に「dissolve」が含まれる場合):
- 推奨ヘルパー:
python3 scripts/end_and_cleanup.py --session-model '${session_model}' --dissolve --body 'Handing back to CLI. Dissolving chat group...'--bodyで終了時のテキストを指定し、--dissolveにより無効化の後に remove-user / dissolve-chat / cleanup-sessions を実行します。 - 手動手順:
- Lark に引き継ぎカードを送信します(無効化する前に実行してください):
python3 scripts/handoff_ops.py send-status-card end --session-model '${session_model}' --body 'Handing back to CLI. Dissolving chat group...' - この引き継ぎのセッションタブを削除します(ツール/モデル):
python3 scripts/handoff_ops.py tabs-end --session-model '${session_model}' - 溶解の手順で使うため、無効化の出力にある
chat_idを控えます。 - 引き継ぎを無効化します(ローカル DB からセッションを削除します):
python3 scripts/handoff_ops.py deactivate - チャットリストから表示されなくなるよう、ユーザーをグループから削除します:
python3 scripts/handoff_ops.py remove-user --chat-id '<CHAT_ID>' - チャットグループを溶解し、残っているセッションをすべてクリーンアップします:
python3 scripts/handoff_ops.py dissolve-chat --chat-id '<CHAT_ID>' python3 scripts/handoff_ops.py cleanup-sessions --chat-id '<CHAT_ID>' - ターミナル通知を復元します:
python3 scripts/iterm2_silence.py off - ローカルで「Handoff ended. Chat group dissolved. Back to CLI.」と出力します。
- ループを停止します。
- Lark に引き継ぎカードを送信します(無効化する前に実行してください):
ステップ 3:ユーザーのメッセージを処理する
Lark からの返信を、ユーザーが CLI に入力したものとして扱います。必要な作業(ファイルの読み取り、コードの編集、コマンドの実行、質問への回答など)を行ってください。
作業したふりをしないルール(厳守):
- アクション(読み取り/編集/実行/デバッグ/レビュー)を求める Lark メッセージには、返信する前に実際にツールを使って作業してください。
- 実際に時間のかかる処理が進行中でない限り、「対応中です」「まだ作業中です」などの仮の返信を送らないでください。
- 進捗を報告する場合は、現在実行中の具体的な作業(コマンド/ファイル/タスク)を記載してください。
- ハンドオフが有効な間は、作業を続けるには CLI に戻る必要があると決して主張しないでください。
- 作業が妨げられている場合は、妨げとなっている理由を伝え、Lark で具体的な質問を 1 つしてください。
スレッドへの返信(parent_id):返信に parent_id が含まれている場合、ユーザーはチャット内の特定のメッセージに返信しています。親メッセージを取得して、文脈を把握してください。カード(インタラクティブ)メッセージは Lark API から劣化した内容が返されるため、次の 2 段階の手順を使ってください。
- まずローカルを検索 —
record_sent_message()がボット送信メッセージ(カードメッセージの元のテキスト/タイトルを含む)をすべて保存するmessagesテーブルを確認します。python3 scripts/handoff_ops.py parent-local --parent-id '<PARENT_ID>' - 見つからない場合は Lark API にフォールバック — 親メッセージがローカルで見つからない場合(ボットではなく別のユーザーからのメッセージの場合):
python3 scripts/handoff_ops.py parent-api --parent-id '<PARENT_ID>'
ユーザーの返信にある「これ」「それ」「あれ」が何を指しているかを把握するために、親メッセージの内容を使ってください。
画像メッセージ:返信に image_key(msg_type: "image" またはインライン画像を含む post)が含まれている場合は、処理する前に画像をダウンロードしてください。複数のインライン画像を含む post では、image_key にカンマ区切りで複数のキーが含まれる場合があります。その場合は、それぞれダウンロードしてください。
python3 scripts/handoff_ops.py download-image --image-key '<IMAGE_KEY>' --message-id '<MESSAGE_ID>'
次に、Read ツールでダウンロードした画像ファイルを読み取り、内容を確認してください。ユーザーは、分析対象のスクリーンショット、Figma のデザイン、エラー画面などを送信することがあります。
ファイルメッセージ:返信の msg_type: "file" に file_key が含まれている場合は、処理する前にファイルをダウンロードしてください。
python3 scripts/handoff_ops.py download-file --file-key '<FILE_KEY>' --message-id '<MESSAGE_ID>' --file-name '<FILE_NAME>'
次に、Read ツールでダウンロードしたファイルを読み取ってください。ユーザーは、コードファイル、ログ、設定ファイル、ドキュメントなどを送信することがあります。
転送されたメッセージ:返信の msg_type: "merge_forward" は、ユーザーが別のチャットの会話スレッドを転送したことを示します。Lark API で子メッセージを取得してください。
python3 scripts/handoff_ops.py merge-forward --message-id '<MESSAGE_ID>'
JSON 出力を解析してください。各行には 1 件のメッセージが含まれています。会話の文脈として、ユーザーにその内容を提示してください。子メッセージに画像(msg_type: "image")が含まれている場合は、上記の画像ダウンロード手順を使って、その子メッセージの message_id で画像をダウンロードしてください。依頼に応じて、スレッドを要約または分析してください。
作業中カード:PostToolUse フックは、ツールの実行中に Lark で「Working...」カードを自動的に送信し、更新します。カードのタイトルは経過時間に応じて(「Working...」→「Working hard...」など)変わり、「Stop」ボタンも表示されます。進捗を知らせる別のメッセージを送る必要はありません。フックが進捗表示を処理します。返信を送信すると、カードは「Done ✓」に更新されます。
ステップ 4:応答を送信し、次のメッセージを待つ
作業が完了したら、send_and_wait.py を使って Lark に応答を送信します。このスクリプトはメッセージを送信し、次のユーザーメッセージが届くまで待機します。出力されるのは次のユーザーメッセージです(wait_for_reply.py と同じ JSON 形式)。
python3 scripts/send_and_wait.py '<your response>'
この呼び出しは「Sent.」を返しません。次のユーザーメッセージを返すまで待機します。 JSON 出力をステップ 1 と同じ方法で解析し、この新しいメッセージでステップ 2 に進みます。
takeoverの場合:ステップ 1 と同様に、何も送信せず Handoff モードを終了します。timeoutの場合:メッセージはすでに送信されています。再送信せず、wait_for_reply.pyを呼び出して待機を再開します。- ユーザーが割り込んだ場合(Esc/Ctrl+C):メッセージはすでに送信されています。ステップ 1 と同様に「Handoff を終了しますか?」と尋ねます。
- 返信が見つかった場合:すべての返信テキストを連結し、ユーザーメッセージとして扱います。
mentions配列を確認します。テキストでは@_user_1のようなプレースホルダーが @ メンションに使われています。各返信のmentions配列(key、id/open_id、name)を使って実際の情報に置き換えます。ステップ 2 に進みます。
形式のオプション(send_to_group.py と同じフラグです):
- Markdown カード(デフォルト。
--cardなし):回答、説明、質問、確認、分析結果など、会話形式のすべての応答に使用します。これがデフォルトです。 - ステータスカード(
--card):簡潔なシステムメッセージにのみ使用します。 - フォームカード:選択肢やテキスト入力には、
handoff_ops.py send-form-select/send-form-inputを使い、その後wait_for_reply.py --timeout 0を実行してフォームへの回答を取得します。処理を続け、最終応答にはsend_and_wait.pyを使います。
メッセージにコードブロックを含める場合は、モバイルで読みやすいように 2 つのスペースでインデントしてください。
メッセージは簡潔にしてください。Lark にはサイズ制限があります。出力が長い場合は要約し、詳細は CLI で確認できることを伝えてください。
Agent Teams との連携
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 が設定されている場合、Handoff モード中にリードエージェントがエージェントチームを作成して調整できます。
チームリクエストの検出
ステップ 2(コマンドの確認)で、ユーザーがチームの作成、チームの状況確認、またはチームメイトとの連絡を求めていないか検出します。例:
- 「認証モジュールをリファクタリングするために 3 人のチームを作成して」
- 「チームの状況」/「チームの進捗はどうですか?」
- 「レビュー担当者にセキュリティを重視するよう伝えて」
- 「チームを終了して」
チームの作成
TeamCreate ツールを直接使用します。ユーザーの Lark メッセージに書かれた作業内容を、TeamCreate のプロンプトに変換します。実行の流れの例:
- Lark のユーザー:「auth モジュールをリファクタリングする 3 人のチームを作成して」
- リードが適切なプロンプトとメンバー数を指定して
TeamCreateを呼び出す - PostToolUse フックがチーム作成イベントを自動的に Lark に転送する
- リードがチームメンバーの名前を添えて Lark に確認を送信する
チームの進捗状況の監視
team_status.py を使ってチームとタスクの状態を読み取り、更新情報を送信します。
# List active teams
python3 scripts/team_status.py list
# Detailed status with progress bar
python3 scripts/team_status.py status <team_id>
# Format as Lark card content
python3 scripts/team_status.py card <team_id>
ユーザーがチームの状況を尋ねたら、team_status.py card <team_id> を実行し、出力をカードとして送信します。
python3 scripts/send_to_group.py "$(python3 scripts/team_status.py card <team_id>)" --title 'Team Progress'
チームメイトとの連絡
SendMessage を使って、Lark のユーザーからのメッセージを特定のチームメイトに伝えます。
- 「コーダーに TypeScript を使うよう伝えて」→「coder」チームメイトに
SendMessage - 「チームメイト全員に進捗状況を尋ねて」→
SendMessageでブロードキャスト
チームメイトの応答は共有タスクリストに届きます。チームでの作業に時間がかかる場合は、team_status.py status を定期的に確認し、進捗状況を Lark に送信します。
Handoff 中のチームのライフサイクル
- 作成:TeamCreate がチームメイトをローカルプロセスとして起動します。各自が独立して作業します。
- 監視:リードが定期的にタスクの進捗を確認し、Lark に報告します。
- 完了:すべてのタスクが完了したら、リードが結果を要約して Lark に送信します。
- クリーンアップ:チームの作業が完了したら、
TeamDeleteを使います。 - 引き継ぎ:チームが活動中に引き継ぎが発生した場合、チームメイトがローカルで引き続き実行中であることをユーザーに警告します。
PostToolUse の転送
フックは、メッセージのフィルターレベルに関係なく、次のチームイベントを Lark に自動転送します。
TeamCreate— メンバー名を含むチームの作成SendMessage— エージェント間メッセージ(要約のみ)TeamDelete— チームの解散
重要な注意事項
- すべての通信は Lark を経由します。 すべての回答、質問、確認、ステータス更新は、必ず Lark のスレッドに送信してください。CLI にはローカル参照用として同じ出力が表示されますが、ユーザーが見ているのは Lark です。
- ループでは実作業を優先します。 Lark の返信で実作業(コード編集、デバッグ、ファイルの読み取り、コマンド実行、レビュー、分析)が求められた場合は、Step 4 で結果を送信する前に、まず Step 3 の作業を完了してください。これらを、稼働確認やステータスのみの返信に置き換えてはいけません。
- 稼働確認の範囲は限定的です。 「ループを続ける」「待機を続ける」といった動作は、アイドル期間に限られます。Lark からの未対応のユーザータスクを優先してはいけません。
- CLI が必須だと誤って伝えないでください。 ハンドオフが有効な間、コーディングには CLI に戻る必要があると主張してはいけません。現在のハンドオフセッション内で直接コード作業を行ってください。
- ハンドオフループ中は、AskUserQuestion や EnterPlanMode を絶対に使用しないでください。 これらのツールは CLI にのみプロンプトを表示するため、ハンドオフモード中のユーザーには見えません。代わりに、
send_and_wait.py(返信も待機します)を使って質問を Lark に送信してください。ユーザーが数字だけで返信できるよう、質問には番号付きの選択肢を付けてください。(注:AskUserQuestion は、ハンドオフループに入る前に行う Guided Setup の CLI モードでのセットアップ(Steps 1-4)では使用します。) - 複数人グループでのオーナーのプライバシー。 グループにゲストやオーナー以外のメンバーがいる場合、Lark のメッセージでオーナーの個人情報を絶対に明かしてはいけません。これには、氏名、システムのユーザー名、ファイルパス(例:
/Users/xxx/)、メールアドレス、環境から推測される個人を特定できる情報が含まれます。オーナーのことは Lark の表示名、または単に「オーナー」と呼んでください。システムレベルの詳細(パス、設定の場所など)は CLI の出力だけに記載してください。 - 複数人グループでの @ メンションの宛先指定。 グループにボット以外の人間が 2 人以上いて、特定の人(例:質問した人、その人のためにタスクを完了した場合、またはその人の依頼に回答する場合)に向けた返信をする場合は、必ず
send_and_wait.pyまたはsend_to_group.pyで--mention-user-id '<their_open_id>'を使用して、その人に @ メンションしてください。これにより、適切な人に通知が届きます。返信するメッセージのsender_idフィールドからopen_idを取得してください。グループに人間が 1 人(オーナーのみ)の場合は、@ メンションは不要です。 - 確認と許可: 破壊的または取り消せない操作(git push、マージ、削除など)を行う前に、Lark に確認メッセージを送り、ユーザーの返信を待ってください。Lark で明示的な確認を得るまでは、絶対に実行してはいけません。
- 選択肢の提示形式 — 適切な形式を選んでください。
- 選択肢が 2 つ(はい/いいえ、承認/拒否など)の場合: ボタンカード(
buttonsを指定したbuild_card)を使用します。タップするだけでよく、送信は不要です。 - 選択肢が 3 つ以上の場合: フォームカード(
selectsを指定したbuild_form_card)を使用します。ドロップダウンメニューから簡単に選択できます。ユーザーがドロップダウンを開かなくても選択肢を確認できるよう、カード本文に必ず選択肢をテキストで記載してください。
コールバックは、選択された値(複数フィールドの場合は JSON)をpython3 scripts/handoff_ops.py send-form-select --title '<TITLE>' --body '**Options:\n1. Option A — description\n2. Option B — description\n3. Option C — description' --field-name choice --placeholder 'Select...' --options-json '[["Option A","a"],["Option B","b"],["Option C","c"]]'textに含むmsg_type: "form_action"として届きます。 フォームの下にキャンセルボタンを表示するには、--cancel-label Cancelを追加します。クリックすると、text: "__cancel__"を含むmsg_type: "button_action"としてコールバックが届きます。 - 代替手段: ユーザーがカードを使わず、テキストで数字を返信した場合も解析してください。
- 選択肢が 2 つ(はい/いいえ、承認/拒否など)の場合: ボタンカード(
- テキスト入力の収集 — ユーザーに何か(コミットメッセージ、検索クエリなど)を入力してもらう場合は、入力フィールド付きフォームカードを使用してください。
入力された値を含むpython3 scripts/handoff_ops.py send-form-input --title '<TITLE>' --body '<PROMPT_TEXT>' --field-name '<FIELD_NAME>' --placeholder '<PLACEHOLDER>'msg_type: "form_action"としてコールバックが届きます。ユーザーはカードを使わず、テキストメッセージで返信することもできます。 - タスクで複数のツール呼び出しが必要な場合は、すべて実行してから、まとめて 1 件の返信を Lark に送信してください(ツール呼び出しごとに個別のメッセージを送信しないでください)。
- タイムアウト処理:
wait_for_reply.pyとsend_and_wait.pyは、デフォルトで GPT モデルの場合は 540 秒、それ以外は 0(無制限) に設定されており、返信が届かない場合は{"timeout": true}を返して正常終了します。- 重要: タイムアウトした場合は、ユーザーにステータスメッセージを送信せず、直ちにスクリプトを再実行してください。これは通常のアイドル動作であり、終了の合図ではありません。
- 絶対に守ること: アイドル期間中に「待機中です」や「引き続き対応可能です」といったメッセージを送信してはいけません。何も送信せず、そのまま待機を続けてください。
- 例外: ステータス更新を送信できるのは、(a)意味のある状態変化があった場合、(b)ユーザーから明示的に求められた場合、(c)時間のかかる作業(60 秒超)を実行中の場合のみです。
send_and_wait.pyがタイムアウトした場合、メッセージはすでに送信済みです。再送信せず、wait_for_reply.pyを呼び出して待機を再開してください。
- 「XX を送って」=ファイル添付です。 ユーザーがファイルの送信を依頼した場合(例:「SKILL.md を送って」)、
handoff_ops.py send-fileを使ってアップロードし、送信してください。ファイルの内容をテキストとして貼り付けてはいけません。 - 画像とファイルの送信。
HANDOFF_SESSION_IDから正しいチャットを特定するhandoff_ops.pyのコマンドを必ず使用してください。
画像やファイルの送信に、その場しのぎの Python コードを絶対に書かないでください。複数のセッションがある場合、python3 scripts/handoff_ops.py send-image /path/to/image.png python3 scripts/handoff_ops.py send-file /path/to/file.pdf [--file-type pdf]get_active_sessions()[0]が誤ったグループを選ぶ可能性があります。 - コミットハッシュのリンク。 Lark のメッセージでコミットハッシュに言及する場合、GitHub のコミットページへのクリック可能なリンクとして記載してください:
hash。git remote get-url originから org/repo を取得してください。 - リアクションのルーティング。
send_to_group.pyがメッセージを送信すると、register_message()を介して、そのメッセージがワーカーに自動登録されます。これにより、ボットのメッセージへの絵文字リアクションをワーカーがハンドオフセッションにルーティングできるようになります。リアクションは、絵文字の種類(例:"THUMBSUP")をtextに含むmsg_type: "reaction"として届きます。特別な処理は不要です。これは自動的に機能します。 - スタンプとリアクションのマナー。 自然に振る舞い、機械的な対応は避けてください。
- リアクションを受け取った場合(例:自分のメッセージに 👍):これは単なる確認です。通常、返信は不要です。同じスタンプを絶対に返してはいけません。
- スタンプメッセージを受け取った場合: 文脈に応じて対応してください。完了報告の後に届いた親指を立てたスタンプは、「よくできました。そのまま続けてください」という意味です。返信せずに待機するか、作業を続けるのが適切です。伝えるべきことがある場合に限り、返信してください。
- こちらからリアクションを送る場合: 対応中であることを示すため、ユーザーのメッセージにリアクションしてください。
lark_im.add_reaction(token, message_id, emoji_type)を使用します。よく使われる種類と用途:THUMBSUP— 依頼を確認し、理解したことを伝えるDONE— タスクが正常に完了したことを伝えるMUSCLE— 難しい作業に取りかかることを伝えるOK— 簡単な確認を伝えるLAUGH/LOL— 軽い雰囲気のやり取りや、面白いことに反応するFACEPALM— うっかりミスをしたときに使うTHINKING— 複雑な問題を分析していることを伝えるLOVE/FINGERHEART— ユーザーが親切なことや役立つことをしてくれたときに使うAPPLAUSE— 節目や良い知らせを祝うSOB— 問題が起きたときに、相手の苛立ちに寄り添うJIAYI(+1)— ユーザーの提案に賛成する
- スタンプで返信する場合: より豊かな表現には
lark_im.reply_sticker(token, message_id, file_key)を使用します。ユーザーが送ったスタンプメッセージからfile_keyを取得してください(file_keyはメッセージのペイロードに含まれています)。一度確認したものはキャッシュして再利用してください。 - スタンプやリアクションは、控えめに、しかし自然に使ってください。スタンプをそのまま返すのは気まずくなります。タイミングのよいリアクションは、テキストの返信より多くを伝えます。
- ワークスペース ID は、すべてのスクリプトで
lark_im.get_workspace_id()を使って自動計算されます。これは物理的なコードの場所(マシンとフォルダーパス)を識別します。
アーキテクチャ
You (Lark) --> Lark Event --> Cloudflare Worker (Durable Objects)
^
Claude Code connects via WebSocket | (HTTP long-poll fallback)
Claude Code sends responses via Lark IM API --> Lark Group
3 つのコンポーネントが連携して動作します。
- Lark App — グループチャットに参加するボットです。イベントを受信し、IM API を使ってメッセージを送信します。
- Cloudflare Worker — Lark の webhook イベントとカードアクションのコールバックを受信し、返信を Durable Objects に保存します。WebSocket(推奨。Hibernation API を使用)または HTTP のロングポーリング(フォールバック)で返信を配信します。
- Claude Code hooks & scripts — 通知、権限ブリッジ、セッションのライフサイクルフック、および引き継ぎモードのスキルです。
トラブルシューティング
Worker に接続できない
~/.handoff/config.jsonのworker_urlが正しいことを確認します。- テスト:
curl -s -H "Authorization: Bearer <your-api-key>" 'https://<your-worker>.workers.dev/poll/test?timeout=1' - 次の結果が返されるはずです:約 1 秒後に
{"replies":[],"count":0} Unauthorizedが返された場合は、設定ファイルのworker_api_keyが Worker のシークレットAPI_KEYと一致していることを確認します。
画像のダウンロードで JSON エラーが返る
- 画像 API では、画像エンドポイントではなくメッセージリソースのエンドポイントが必要です。
- 正しい例:
/im/v1/messages/{message_id}/resources/{image_key}?type=image - 誤った例:
/im/v1/images/{image_key}(権限エラーが返されます) - アプリに
im:resourceスコープが付与されていることを確認します。
返信が届かない
- Lark アプリのイベントサブスクリプション URL が Worker(
/webhook)を指していることを確認します。 - カードコールバック URL が設定されていることを確認します(
/card-action)。 - ボットがグループチャットに追加されていることを確認します。
- 引き継ぎ用グループ(プロジェクトごとに専用のグループが割り当てられます)でメッセージを送信していることを確認します。
- Worker のログを確認します:
npx wrangler tail
トークンエラー
~/.handoff/config.jsonの認証情報でapp_idまたはapp_secretが正しくない可能性があります。- トークンは 2 時間ごとに期限切れになります。スクリプトが自動で更新します。
ファイル一覧
| ファイル | 目的 |
|---|---|
.claude/skills/handoff/SKILL.md | ハンドオフモードのスキル定義 |
.claude/skills/handoff/SKILL-setup.md | ガイド付きセットアップウィザードの手順 |
.claude/skills/handoff/SKILL-commands.md | サブコマンドの実装(チャット、ステータス、deinit など) |
.claude/skills/handoff/hooks.json | 標準フック定義(唯一の信頼できる情報源) |
.claude/skills/handoff/scripts/lark_im.py | Lark IM API クライアント(トークン、送信、返信、ダウンロード、ポーリング) |
.claude/skills/handoff/scripts/on_notification.py | 通知フック(Lark メッセージを送信) |
.claude/skills/handoff/scripts/on_post_tool_use.py | PostToolUse/PostToolUseFailure フック(ツールの出力とエラーを Lark に転送) |
.claude/skills/handoff/scripts/on_pre_compact.py | PreCompact フック(コンテキストの圧縮時に Lark チャットへ警告) |
.claude/skills/handoff/scripts/on_pre_tool_use_bash.py | PreToolUse フック — ハンドオフがアクティブな場合にのみ Bash を許可 |
.claude/skills/handoff/scripts/permission_bridge.py | 権限ブリッジフック(Lark 経由で許可/拒否)— Claude Code |
.claude/skills/handoff/scripts/permission_core.py | 共有の権限ブリッジ・ポーリング/判定コア(Claude + OpenCode) |
.claude/skills/handoff/scripts/worker_http.py | 共有 urllib ワーカーポーリング/確認応答ヘルパー(OpenCode + ユーティリティ) |
.claude/skills/handoff/scripts/on_session_start.py | セッション開始フック(アクティブなハンドオフを検出) |
.claude/skills/handoff/scripts/on_session_end.py | セッション終了フック(Lark に通知し、クリーンアップ) |
.claude/skills/handoff/scripts/send_to_group.py | ハンドオフグループにメッセージを送信(応答を待たず、ハートビート/ステータス用) |
.claude/skills/handoff/scripts/send_and_wait.py | 応答を送信し、次の返信を待機(メインループのステップ 4) |
.claude/skills/handoff/scripts/wait_for_reply.py | 新しい返信を待機(最初の反復時のみ、または send_and_wait がタイムアウトした後) |
.claude/skills/handoff/scripts/handoff_ops.py | インラインの python スニペットを置き換える、決定論的なヘルパーコマンド |
.claude/skills/handoff/scripts/run_tests.py | CI/ローカルチェック用のワンコマンド・ハンドオフテストランナー |
.claude/skills/handoff/scripts/iterm2_silence.py | iTerm2 のターミナル通知を切り替え |
.claude/skills/handoff/scripts/preflight.py | 事前検証 |
.claude/skills/handoff/worker/src/index.js | Cloudflare Worker(webhook + カードコールバック + Durable Objects) |
.claude/skills/handoff/worker/wrangler.toml | Worker のデプロイ設定 |
~/.handoff/config.json | 認証情報と設定(app_id、app_secret、worker_url、worker_api_key、email) |
~/.handoff/projects/<project>/handoff-data.db | SQLite データベース(ハンドオフの状態 + 双方向のメッセージ履歴) |
${HANDOFF_TMP_DIR:-/tmp/handoff}/handoff-images/ | ダウンロードした画像 |
${HANDOFF_TMP_DIR:-/tmp/handoff}/handoff-files/ | ダウンロードしたファイル |
.opencode/plugins/handoff.ts | OpenCode プラグイン(権限ブリッジ、ライフサイクル、通知、環境変数の注入) |
.opencode/scripts/permission_bridge.py | OpenCode プラグインから呼び出される権限ブリッジスクリプト |
.opencode/scripts/handoff_tool_forwarding.js | OpenCode プラグインが使用するツール出力転送スクリプト |