Claude Media
権限確認プロンプトをChannelsで中継する実装手順(Claude Code)

権限確認プロンプトをChannelsで中継する実装手順(Claude Code)

Claude Code Channelsの権限確認プロンプトをチャットへ中継する実装です。4ステップのリレーの仕組みと、リクエストIDのサニタイズ履歴をコード付きでまとめました。

権限確認プロンプトの中継とは何か

Bashコマンドの実行やファイル編集をClaudeが求めると、ローカルの承認ダイアログが開き、セッションはユーザーの応答を待って止まります。手元にいなければ、この待ち時間はそのまま作業の停止時間になります。権限中継(permission relay)は、同じ承認プロンプトをTelegramやDiscordのようなチャットへも同時に転送し、スマートフォンから承認・拒否できるようにする仕組みです。

中継対象はBash・Write・Editのようなツール使用の承認です。プロジェクトの信頼確認やMCPサーバーの利用同意ダイアログは中継されず、ローカルターミナルにしか出ません。

この記事はChannels reply toolを実装するで組み立てた二方向チャットブリッジに、権限中継を追加する前提で進めます。reply toolがまだ無い場合は先にそちらを実装してください。

中継の4ステップ

手順

権限プロンプトが開いてから判定が戻るまで

  1. 1

    IDを発行して通知する

    Claude Codeが5文字のリクエストIDを生成し、チャネルサーバーに通知します。

  2. 2

    チャットへ転送する

    サーバーがプロンプトとIDをチャットアプリへ転送します。

  3. 3

    IDを添えて返信する

    リモートのユーザーがそのIDを添えて、yesかnoを返信します。

  4. 4

    判定として適用する

    サーバー側のハンドラーが返信を判定に変換します。Claude Codeは、開いているリクエストのIDと一致する場合だけそれを適用します。

ローカルのターミナルダイアログはこの間ずっと開いたままです。ターミナルで先に応答すればそちらが優先され、リモートからの保留中リクエストは破棄されます。逆にリモートから先に応答が届けば、それが適用されてローカルダイアログは閉じます。早い者勝ちで、どちらか一方だけが効きます。

Claude Code v2.1.234以降は、そのセッションでチャネルとして登録されたサーバーにしか権限リクエストを送りません。したがって中継は、メッセージ配信と同じ--channelsによるセッションのオプトインと組織のポリシー(Enterprise controls)の両方に縛られます。加えて、サーバー側が権限capabilityを宣言していなければ中継自体が始まりません。

リクエストの中身とサニタイズ

Claude Codeから送られる通知はnotifications/claude/channel/permission_requestで、4つの文字列フィールドを持ちます。

フィールド内容
request_id内容a-zからlを除いた小文字5文字。電話で入力しても1やIと誤読しない設計
tool_name内容BashやWriteなど、Claudeが使おうとしているツール名
description内容何をする呼び出しかの要約。Bashではコマンドの説明文で、Claudeが説明を付けなければ定数Run shell commandになり、コマンド自体の情報は一切含まれない
input_preview内容ツール引数をJSON形式に近い表示テキストにしたもの。Bashならコマンド本体、Writeならファイルパスと内容

descriptionはコマンドの詳細を保証しないため、承認可否を判断できる情報はinput_preview側にあります。表示領域が許すならinput_previewも一緒に出すのが安全です。

このフィールドには2段階のサニタイズ強化が入っています。

  • v2.1.211以降: 方向反転文字・不可視文字・引用符や山かっこの見た目を似せた文字を無害化し、空白の連続を1つに畳む。3,500コードポイントを超える値は先頭と末尾を残して⋯ N code points elided ⋯で省略する。input_previewはトップレベルのフィールドごとにこの上限を適用する
  • v2.1.234以降: 循環構造や極端に大きな配列など安全にシリアライズできないinput_previewのフィールド値は(value unserializable)という文字列で中継される。この場合もキー自体は届き、他のフィールドの値は変わらない。加えて、APIキーや個人アクセストークンのような認証情報らしき文字列を[REDACTED]にマスクする。このマスキングはキー名自体も対象になるため、表示されたキー名が実際の入力のキー名と一致しないことがある。ただしシェル構文・パス文字・URL文字を含む範囲はマスクされない(コマンドや宛先そのものを隠さないため)。プレフィックスを持たない秘密情報や、複数行にまたがる秘密鍵ブロックのようなものはマスクされずそのまま届く

秘密鍵ブロックの扱いは、公式ページ同士で書き方が違います。チャネル仕様のページには「複数行にまたがる秘密鍵ブロックはマスクされない」とあります。一方v2.1.234のchangelogには「大きすぎる秘密鍵ブロックは強いマスクで伏せられる」とあります。どちらの挙動でも届く可能性があるので、秘密鍵は載らない前提で組みます。

v2.1.211より前のクライアントはdescriptionを無加工で中継し、input_previewは200 UTF-16単位で切り詰めていました。中継先のクライアントバージョンを制御できない環境では、この2フィールドを常に信頼できない入力として扱うのが安全です。マスクは配信先の制限とは別物で、これらのフィールドが届くのはあくまで--channelsか開発フラグでオプトインしたサーバーだけです。

request_idはローカルのターミナルダイアログには表示されません。このIDを知る手段はチャネルサーバーの通知ハンドラーだけなので、サーバー側でプロンプトにIDを含め忘れると、リモートのユーザーはどのリクエストに答えればいいか分からなくなります。ログにIDを残しておくと、後から「どの承認がどのツール呼び出しに対応していたか」を追いやすくなります。

返信側の判定はnotifications/claude/channel/permissionで送ります。request_id(エコーバック)とbehavior('allow'または'deny')の2フィールドだけです。allowはツール呼び出しを進め、denyはツール呼び出しを拒否します。どちらの判定も以降の呼び出しには影響しません。

チャットブリッジに権限中継を足す

実装は3つの部品でできています。

  1. Serverコンストラクタのexperimentalcapabilitiesにclaude/channel/permission: {}を追加する
  2. notifications/claude/channel/permission_requestのnotificationハンドラーを登録し、プラットフォームへプロンプトを整形して送る
  3. 受信メッセージハンドラーに、yes <id>/no <id>形式を検出して判定通知を送るチェックを差し込む

capabilityを宣言する

capabilities: {
  experimental: {
    'claude/channel': {},
    'claude/channel/permission': {},  // 権限中継にopt-in
  },
  tools: {},
},

Claude Code v2.1.234より前は、この値をfalseにしてもcapabilityが「宣言されている」扱いになるバグがありました。オプトアウトしたい場合はキー自体を省略します。

受信ハンドラーを書く

import { z } from 'zod'
 
const PermissionRequestSchema = z.object({
  method: z.literal('notifications/claude/channel/permission_request'),
  params: z.object({
    request_id: z.string(),
    tool_name: z.string(),
    description: z.string(),
    input_preview: z.string(),
  }),
})
 
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
  send(
    `Claude wants to run ${params.tool_name}: ${params.description}\n` +
    `${params.input_preview}\n\n` +
    `Reply "yes ${params.request_id}" or "no ${params.request_id}"`,
  )
})

このZodスキーマのmethodが、どの通知に反応するかを決めます。paramsの型検証も同じスキーマが担います。

返信を判定として解釈する

// "y abcde" / "yes abcde" / "n abcde" / "no abcde" にマッチ
// [a-km-z]はClaude Codeが使うID用アルファベット(小文字、lを除く)
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
 
async function onInbound(message: PlatformMessage) {
  if (!allowed.has(message.from.id)) return  // 送信元の検証が先
 
  const m = PERMISSION_REPLY_RE.exec(message.text)
  if (m) {
    await mcp.notification({
      method: 'notifications/claude/channel/permission',
      params: {
        request_id: m[2].toLowerCase(),
        behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
      },
    })
    return  // 判定として処理したので通常チャットへは転送しない
  }
 
  // 判定形式にマッチしなければ、通常のチャット転送に回す
  await mcp.notification({
    method: 'notifications/claude/channel',
    params: { content: message.text, meta: { chat_id: String(message.chat.id) } },
  })
}

/iフラグはスマートフォンの自動大文字化を許容するためで、送り返す前に必ず小文字化します。返信の形とIDの一致で、行き先は次の3通りに分かれます。

分岐

返信が届いたときの3つの行き先

  • 形式もIDも合う

    判定として適用され、ローカルのダイアログが閉じます。

  • 形式が合わない

    approve itのような自然文や、IDなしのyesです。通常のチャットメッセージとしてClaudeへ転送され、ダイアログは開いたままです。

  • 形式は合うがIDが違う

    Claude Codeがその判定を黙って破棄します。こちらもダイアログは開いたままです。

動作確認 — 手動モードで承認ダイアログを開く

自動モードだと分類器がツール呼び出しの可否を先に決めてしまい、承認ダイアログ自体が開きません。中継の動作を確認するには、セッションを手動モードに切り替えます。3つのターミナルを使って、セッション起動・イベント購読・指示送信・承認返信の流れを順に確認します。

claude --dangerously-load-development-channels server:webhook
Shift+Tabを押し続けてステータスバーが「⏸ manual mode on」になるまで切り替える

別ターミナルでイベントストリームを購読し、3つ目のターミナルからファイル一覧のような読み取り専用の指示を送ります。

curl -d "list the files in this directory" -H "X-Sender: dev" localhost:8788

-H "X-Sender: dev"は送信元を示すヘッダーですが、reply toolを実装する記事のサンプルサーバーには送信元を検証する処理が無いため、このヘッダー自体は動作に影響しません。公式の完成版webhook.tsは、dev以外を403で拒否します。読み取りだけなら承認なしで実行されますが、そのあとClaudeがreplyツールで結果を返そうとした瞬間に権限ダイアログが開き、/events側にリクエストIDが流れます。3つ目のターミナルから、そのIDを添えて承認を返します。

curl -d "yes <id>" -H "X-Sender: dev" localhost:8788

このリクエストが届くと、ローカルのダイアログが閉じてreplyツールが実行され、Claudeの返信がストリームに乗ります。

プロンプトが届かないときの切り分け

中継は複数の条件が重なって初めて動きます。ローカルのダイアログは開くのにチャットへ何も来ないときは、手前から順に見ます。

症状確認する点
起動時の通知に自分のサーバーが出ない確認する点--channelsか開発フラグで指定したか。許可リスト外のプラグインはチャネルとして登録されない
Team・Enterpriseで一切届かない確認する点組織の管理設定channelsEnabledがtrueか。開発フラグも含めて、オフなら全チャネルが止まる
登録は済むのに中継だけ来ない確認する点claude/channel/permissionを宣言したか。falseを書いても、v2.1.234より前は宣言扱いだった
ダイアログ自体が開かない確認する点自動モードでは分類器が先に判定する。手動モードにする

v2のMCPクライアントランタイムには、もう1つ落とし穴があります。MCPプロトコルのrevision 2026-07-28を交渉するチャネルサーバーは、メッセージを届けられないためチャネルとして登録されません。登録されなければ、権限リクエストの送り先にもなりません。stdioのチャネルサーバーを従来のハンドシェイクのまま使いたいときは、環境変数MCP_PROTOCOL_NEGOTIATIONをlegacyにします。この変数はv2.1.221以降で使え、v2のランタイムでだけ有効です。v2.1.285以降は、Claude Codeがフィーチャーフラグを取得するセッションで、この交渉が既定でオンになります。stdioのチャネルサーバーが新しいrevisionを交渉する構成だと、既定では登録されない可能性があります。

MCP_PROTOCOL_NEGOTIATION=legacy claude --dangerously-load-development-channels server:webhook

よくあるつまずき

input_previewを省略してdescriptionだけ表示する

Bashの呼び出しでClaudeが説明を付けなかった場合、descriptionは定数Run shell commandになり、コマンド自体の情報を一切含みません。表示領域に余裕があるならinput_previewも一緒に送り、判断材料ゼロで承認を迫る事態を避けます。

z.string()で受けた値をそのまま信頼する

Zodスキーマは型のバリデーションであって内容の安全性は保証しません。descriptionとinput_previewはサニタイズ済みでも「信頼できない入力」として扱い、プラットフォーム側の表示エスケープを別途行います。

判定処理を通常チャットの転送より後ろに置いてしまう

受信ハンドラーではPERMISSION_REPLY_REのマッチ確認を先に行い、一致したらそこで処理を終えてチャットへは転送しません。この順序を逆にすると、yes abcdeのような返信がそのままClaudeへの通常メッセージとして渡ってしまい、判定として扱われません。

権限中継が前提にしているもの

権限中継はメッセージ配信より一段強い権限をチャネルに与えます。承認できる相手を増やすということは、セッションを乗っ取れる相手を増やすことと同義です。

公式チャネルの送信者許可リストも、チャネルが権限中継を宣言していれば中継を縛ります。だからこそ公式ドキュメントも「送信元を検証済みのチャネルでだけ宣言する」ことを明記しています。

Telegram・Discordのペアリングや、from.idとchat.idの取り違えといった落とし穴は、Channelsの受信メッセージをゲートする設計で扱っています。

権限中継そのものはv2.1.81で入りました。実質的な信頼境界は、その後のセキュリティ修正が作っています。

あゆみ

権限中継まわりの変更履歴

  1. v2.1.81`--channels`の権限中継を追加

    権限capabilityを宣言したチャネルサーバーへ、ツール承認プロンプトを転送できるようになりました。

  2. v2.1.211中継テキストの無害化

    方向反転文字・ゼロ幅文字・引用符・山かっこの類似文字を無害化する修正です。ツール入力が承認メッセージの見た目を書き換えられないようにする目的で入りました。

  3. v2.1.234信頼ゲートと認証情報のマスク

    登録済みのチャネルサーバーにだけ中継するようになりました。認証情報のマスクはコマンド・パス・宛先を隠さず、APIトークンの直後にシェルの区切り文字が続く場合も対象です。

  4. v2.1.281`--channels`のプラグイン指定を厳格化

    インストール済みプラグインの名前も、--channelsのエントリと一致する必要があります。

まとめ

始めやすいのは、受信ゲートが実装済みのチャネルと、v2.1.234以降のクライアントの組み合わせです。

よくある質問

request_idをログに残しても安全ですか

IDだけなら5文字の文字列で、Claude Codeは自分が発行したIDの判定しか受け付けません。ただしdescriptionやinput_previewは機密情報を含むことがあるため、ログに残す場合は別途マスキングを検討してください。

Bedrock経由のセッションでも権限中継は使えますか

使えません。Channelsそのものが、Amazon Bedrock・Google Cloud's Agent Platform・Microsoft Foundry経由の認証に対応していないためです。そのため権限中継を含むChannelsの全機能は、claude.aiアカウントかConsole APIキーでの利用に限定されます。

この記事を共有:XはてブLinkedIn