本文へ移動
hanzochang

技術と実践のノート

OpenClaw 通信・接続の仕組み - なぜDiscordのメッセージでローカルのbashが動くのか

公開

著者:株式会社hanzochang

OpenClawはなぜDiscordやSlackからローカルのbashを操作できるのか?外向きWebSocket接続の仕組み、Gateway内部のメッセージフロー、Node.jsプロセスの権限継承まで、ソースコードを追いながら通信経路を解説します。

はじめに

OpenClawは、ローカルマシン上で動くパーソナルAIアシスタントのOSSです。Discord、Slack、Telegram、WhatsAppなど38以上のメッセージングプラットフォームを1つのAIが横断的に対応します。

OpenClawの構造は5つのレイヤーで構成されています。


Generated Image A2a19313 9b57 4e25 8fa5 35f75d23a79a A2a19313 9b57 4e25 8fa5 35f75d23a79a
  1. Layer 1: Input Sources — Discord、Slackなど各メッセンジャーとの接続
  2. Layer 2: WebSocket Gateway — ローカルで動作するWebSocketサーバー。メッセージのルーティングやセッション管理
  3. Layer 3: Agent Runtime — LLMにプロンプトを送り、ツール(bash、ファイル操作など)を実行するエージェント本体
  4. Layer 4: LLM Providers — Anthropic、OpenAI、Geminiなど複数のLLMプロバイダーへの接続
  5. Layer 5: Output & Storage — セッション永続化、メモリ/RAG、音声合成、ネイティブアプリなどの出力系

この記事では、Layer 1 → Layer 2 → Layer 3 の通信経路に焦点を当て、以下の疑問にソースコードを追いながら答えます。

  • DiscordやSlackのメッセージが、なぜローカルマシンに届くのか
  • なぜファイル操作やbashコマンドの実行が可能なのか
対象読者
  • 「外部サービス→ローカル実行」の通信設計を理解したい方
  • AIエージェントのネットワーク構成に興味がある方

目次

前提:OpenClawは外向き接続(Outbound)である

DiscordやSlackからローカルマシンに「直接アクセス」しているわけではありません。OpenClawのほぼすべてのチャネルは、ローカルマシンから外部サーバーへの「外向き接続(Outbound)」で成り立っています。


スクリーンショット 2026 02 16 16.25.28

つまり、外部から自分のマシンへのインバウンド接続は存在しません。NAT配下やファイアウォールの内側でも動作します。

チャネルごとの接続パターン

パターン1: 外向きWebSocket接続

Discord、Slack、WhatsAppはこのパターンです。

Discord

discord.jsライブラリが、Discord公式のGateway API(wss://gateway.discord.gg)へ外向きWebSocket接続を張ります。

typescript
import { Client, GatewayIntentBits } from 'discord.js'

// discord.js が wss://gateway.discord.gg へ outbound WebSocket を張る
const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
})

client.on('messageCreate', async (message) => {
  // Discord → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(message)
  await autoReplyPipeline.handle(msgContext)
})

// ローカル → Discord サーバーへの outbound 接続
client.login(botToken)
typescript
import { Client, GatewayIntentBits } from 'discord.js'

// discord.js が wss://gateway.discord.gg へ outbound WebSocket を張る
const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
})

client.on('messageCreate', async (message) => {
  // Discord → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(message)
  await autoReplyPipeline.handle(msgContext)
})

// ローカル → Discord サーバーへの outbound 接続
client.login(botToken)

Source: extensions/discord/src/monitor.ts

Discordサーバーからの「プッシュ」に見えますが、実際にはOpenClaw側が張ったWebSocketコネクション上でイベントを受信しているだけです。

Slack

@slack/boltのSocket Modeを使用します。これもSlack側のAPIサーバーへの外向きWebSocket接続です。

typescript
import { App } from '@slack/bolt'

// Socket Mode: ローカル → Slack API へ outbound WebSocket を張る
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  appToken: process.env.SLACK_APP_TOKEN,
  socketMode: true, // HTTPサーバーではなくWebSocket接続を使う
})

app.message(async ({ message, say }) => {
  // Slack → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(message)
  await autoReplyPipeline.handle(msgContext)
})

// ローカル → Slack API への outbound 接続を開始
await app.start()
typescript
import { App } from '@slack/bolt'

// Socket Mode: ローカル → Slack API へ outbound WebSocket を張る
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  appToken: process.env.SLACK_APP_TOKEN,
  socketMode: true, // HTTPサーバーではなくWebSocket接続を使う
})

app.message(async ({ message, say }) => {
  // Slack → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(message)
  await autoReplyPipeline.handle(msgContext)
})

// ローカル → Slack API への outbound 接続を開始
await app.start()

Source: extensions/slack/src/monitor.ts

WhatsApp

@whiskeysockets/baileysライブラリが、WhatsApp Webのプロトコルを使用してWhatsAppサーバーへ外向き接続します。初回接続時にQRコードを読み取る必要があります。

typescript
import makeWASocket, { useMultiFileAuthState } from '@whiskeysockets/baileys'

// セッション情報をファイルに永続化(2回目以降はQR不要)
const { state, saveCreds } = await useMultiFileAuthState('./auth')

// ローカル → WhatsApp サーバーへ outbound WebSocket を張る
const sock = makeWASocket({ auth: state })
sock.ev.on('creds.update', saveCreds)

// 初回接続時はQRコードをターミナルに表示
sock.ev.on('connection.update', ({ qr }) => {
  if (qr) printQRCode(qr)
})

sock.ev.on('messages.upsert', async ({ messages }) => {
  // WhatsApp → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(messages[0])
  await autoReplyPipeline.handle(msgContext)
})
typescript
import makeWASocket, { useMultiFileAuthState } from '@whiskeysockets/baileys'

// セッション情報をファイルに永続化(2回目以降はQR不要)
const { state, saveCreds } = await useMultiFileAuthState('./auth')

// ローカル → WhatsApp サーバーへ outbound WebSocket を張る
const sock = makeWASocket({ auth: state })
sock.ev.on('creds.update', saveCreds)

// 初回接続時はQRコードをターミナルに表示
sock.ev.on('connection.update', ({ qr }) => {
  if (qr) printQRCode(qr)
})

sock.ev.on('messages.upsert', async ({ messages }) => {
  // WhatsApp → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(messages[0])
  await autoReplyPipeline.handle(msgContext)
})

Source: extensions/whatsapp/src/monitor.ts

パターン2: 外向きHTTPポーリング

Telegramはこのパターンです。

typescript
import { Bot } from 'grammy'

// Bot Token で Telegram Bot API に接続
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN)

bot.on('message:text', async (ctx) => {
  // Telegram → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(ctx.message)
  await autoReplyPipeline.handle(msgContext)
})

// long polling 開始: ローカル → Telegram API へ getUpdates を送り続ける
bot.start()
typescript
import { Bot } from 'grammy'

// Bot Token で Telegram Bot API に接続
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN)

bot.on('message:text', async (ctx) => {
  // Telegram → OpenClaw auto-reply パイプラインへハンドオフ
  const msgContext = normalizeToMsgContext(ctx.message)
  await autoReplyPipeline.handle(msgContext)
})

// long polling 開始: ローカル → Telegram API へ getUpdates を送り続ける
bot.start()

Source: extensions/telegram/src/monitor.ts

Long pollingなので、ローカルからTelegram APIへHTTPリクエストを送り続け、新着があればレスポンスとして返ってくる仕組みです。

パターン3: 外向き接続 + ローカルプロセス連携

Signal は signal-cli というローカルバイナリ経由で動作します。

typescript
// signal-cli が別プロセスで Signal サーバーと通信(outbound)
// OpenClaw は signal-cli の REST API(localhost:8080)経由でやり取り

const SIGNAL_CLI_API = 'http://localhost:8080/api/v1'

// signal-cli の receive エンドポイントをポーリング
const pollMessages = async () => {
  const res = await fetch(`${SIGNAL_CLI_API}/receive/${phoneNumber}`)
  const messages = await res.json()

  for (const msg of messages) {
    const msgContext = normalizeToMsgContext(msg)
    await autoReplyPipeline.handle(msgContext)
  }
}

setInterval(pollMessages, 1000)
typescript
// signal-cli が別プロセスで Signal サーバーと通信(outbound)
// OpenClaw は signal-cli の REST API(localhost:8080)経由でやり取り

const SIGNAL_CLI_API = 'http://localhost:8080/api/v1'

// signal-cli の receive エンドポイントをポーリング
const pollMessages = async () => {
  const res = await fetch(`${SIGNAL_CLI_API}/receive/${phoneNumber}`)
  const messages = await res.json()

  for (const msg of messages) {
    const msgContext = normalizeToMsgContext(msg)
    await autoReplyPipeline.handle(msgContext)
  }
}

setInterval(pollMessages, 1000)

Source: extensions/signal/src/monitor.ts

パターン4: インバウンドWebhook(例外的)

Google ChatやMicrosoft Teamsはこのパターンです。これらのプラットフォームはWebhookでメッセージを配信するため、外部からOpenClawへのHTTPアクセスが必要です。


Generated Image E955d787 F9ec 4bf8 9523 03bc9335f957 E955d787 F9ec 4bf8 9523 03bc9335f957

この場合はTailscale ServeまたはFunnelを使って、ローカルのGatewayをインターネットから到達可能にする必要があります。

typescript
import { execSync } from 'child_process'

// Gateway は localhost のみにバインド(外部から直接アクセス不可)
const GATEWAY_HOST = '127.0.0.1'
const GATEWAY_PORT = 18789

// Tailscale Serve: tailnet 内の他デバイスからアクセス可能にする
// tailscale serve --bg https+insecure://127.0.0.1:18789
execSync(`tailscale serve --bg https+insecure://${GATEWAY_HOST}:${GATEWAY_PORT}`)

// Tailscale Funnel: インターネット全体に公開(Webhook 受信用)
// tailscale funnel --bg https+insecure://127.0.0.1:18789
execSync(`tailscale funnel --bg https+insecure://${GATEWAY_HOST}:${GATEWAY_PORT}`)

// Google Chat / Teams の Webhook URL:
// https://your-node.tailnet.ts.net/googlechat
typescript
import { execSync } from 'child_process'

// Gateway は localhost のみにバインド(外部から直接アクセス不可)
const GATEWAY_HOST = '127.0.0.1'
const GATEWAY_PORT = 18789

// Tailscale Serve: tailnet 内の他デバイスからアクセス可能にする
// tailscale serve --bg https+insecure://127.0.0.1:18789
execSync(`tailscale serve --bg https+insecure://${GATEWAY_HOST}:${GATEWAY_PORT}`)

// Tailscale Funnel: インターネット全体に公開(Webhook 受信用)
// tailscale funnel --bg https+insecure://127.0.0.1:18789
execSync(`tailscale funnel --bg https+insecure://${GATEWAY_HOST}:${GATEWAY_PORT}`)

// Google Chat / Teams の Webhook URL:
// https://your-node.tailnet.ts.net/googlechat

Source: src/gateway/server-tailscale.ts

接続パターンのまとめ
チャネル接続方向プロトコルポート開放
DiscordOutboundWebSocket不要
SlackOutboundWebSocket (Socket Mode)不要
WhatsAppOutboundWebSocket (Baileys)不要
TelegramOutboundHTTP Long Polling不要
SignalOutboundsignal-cli → Signal不要
iMessageLocalBlueBubbles REST API不要
Google ChatInboundWebhook POST必要
TeamsInboundBot Framework必要
MatrixOutboundHTTP Sync API不要
チャネル接続方向プロトコルポート開放
DiscordOutboundWebSocket不要
SlackOutboundWebSocket (Socket Mode)不要
WhatsAppOutboundWebSocket (Baileys)不要
TelegramOutboundHTTP Long Polling不要
SignalOutboundsignal-cli → Signal不要
iMessageLocalBlueBubbles REST API不要
Google ChatInboundWebhook POST必要
TeamsInboundBot Framework必要
MatrixOutboundHTTP Sync API不要

Gateway内部の通信

チャネルからGatewayへのハンドオフ

外部プラットフォームからメッセージを受信したチャネルプラグインは、Gateway内部のauto-replyパイプラインにメッセージを渡します。この受け渡しはプロセス内の関数呼び出しです。


Generated Image 6f1002a8 De7d 423f 9ca8 C3f7218cc791 6f1002a8 De7d 423f 9ca8 C3f7218cc791

重要なのは、チャネルプラグインとGateway、そしてAgent Runtimeがすべて同一プロセス内で動作している点です。WebSocket経由のプロセス間通信ではなく、TypeScriptの関数呼び出しでメッセージが流れます。

エージェントの呼び出し

GatewayからAgentへのメッセージ受け渡しには2つのモードがあります。

Embeddedモード(デフォルト)

エージェントがGatewayと同一プロセス内で動作します。runEmbeddedPiAgent 関数が直接呼び出され、LLMとのやり取りもこのプロセス内で行われます。

RPCモード

エージェントが別プロセスとして起動され、WebSocket経由でGatewayと通信します。Dockerサンドボックスを使う場合はこのモードになります。

typescript
// Embeddedモード: Gateway プロセス内で直接エージェントを実行
const runEmbeddedPiAgent = async (msgContext: MsgContext) => {
  const messages = buildPrompt(msgContext)
  const response = await llm.chat(messages) // LLM API 呼び出し
  const toolCalls = response.toolCalls ?? []

  for (const call of toolCalls) {
    const result = await executeTool(call) // bash, fs 等のツール実行
    messages.push({ role: 'tool', content: result })
  }

  return await llm.chat(messages) // 最終応答
}

// RPCモード: 別プロセスのエージェントへ WebSocket でリレー
// const ws = new WebSocket('ws://localhost:18790/agent')
// ws.send(JSON.stringify(msgContext))
typescript
// Embeddedモード: Gateway プロセス内で直接エージェントを実行
const runEmbeddedPiAgent = async (msgContext: MsgContext) => {
  const messages = buildPrompt(msgContext)
  const response = await llm.chat(messages) // LLM API 呼び出し
  const toolCalls = response.toolCalls ?? []

  for (const call of toolCalls) {
    const result = await executeTool(call) // bash, fs 等のツール実行
    messages.push({ role: 'tool', content: result })
  }

  return await llm.chat(messages) // 最終応答
}

// RPCモード: 別プロセスのエージェントへ WebSocket でリレー
// const ws = new WebSocket('ws://localhost:18790/agent')
// ws.send(JSON.stringify(msgContext))

Source: src/agents/embedded-agent.ts

なぜbashを実行できるのか

シンプルな答え:Node.jsプロセスの権限

OpenClawのGatewayはNode.jsプロセスとして動作します。このプロセスはあなたのユーザー権限で実行されているため、あなたができることはすべてできます。


Generated Image 8ab14fcb Fa8c 497d Aad6 160fca93a4d2 8ab14fcb Fa8c 497d Aad6 160fca93a4d2 1
コマンド実行の実装

エージェントがbashコマンドを実行する際は、Node.jsの child_process.spawn または node-pty(擬似ターミナル)を使ってサブプロセスを起動します。

typescript
import { spawn } from 'child_process'
import * as pty from 'node-pty'

// execツール: 単発コマンド実行
const execTool = (command: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    const proc = spawn('bash', ['-c', command])
    let output = ''
    proc.stdout.on('data', (data) => {
      output += data
    })
    proc.stderr.on('data', (data) => {
      output += data
    })
    proc.on('close', () => resolve(output))
  })
}

// bashツール: 対話的シェルセッション(node-pty で擬似ターミナル)
const bashSession = pty.spawn('bash', [], {
  name: 'xterm-256color',
  cols: 120,
  rows: 30,
})

// ファイル操作: Node.js の fs モジュールで直接読み書き
// import { readFile, writeFile } from 'fs/promises'
typescript
import { spawn } from 'child_process'
import * as pty from 'node-pty'

// execツール: 単発コマンド実行
const execTool = (command: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    const proc = spawn('bash', ['-c', command])
    let output = ''
    proc.stdout.on('data', (data) => {
      output += data
    })
    proc.stderr.on('data', (data) => {
      output += data
    })
    proc.on('close', () => resolve(output))
  })
}

// bashツール: 対話的シェルセッション(node-pty で擬似ターミナル)
const bashSession = pty.spawn('bash', [], {
  name: 'xterm-256color',
  cols: 120,
  rows: 30,
})

// ファイル操作: Node.js の fs モジュールで直接読み書き
// import { readFile, writeFile } from 'fs/promises'

Source: src/agents/tools/

macOSの権限(TCC)

macOSのネイティブアプリ(メニューバーアプリ)を使う場合は、TCC(Transparency, Consent, and Control)による権限管理があります。

swift
// ノードが公開するケイパビリティ
enum Permission {
  case appleScript      // Automation権限
  case notifications    // 通知
  case accessibility    // アクセシビリティ
  case screenRecording  // 画面録画
  case microphone       // マイク
  case speechRecognition // 音声認識
  case camera           // カメラ
  case location         // 位置情報
}
swift
// ノードが公開するケイパビリティ
enum Permission {
  case appleScript      // Automation権限
  case notifications    // 通知
  case accessibility    // アクセシビリティ
  case screenRecording  // 画面録画
  case microphone       // マイク
  case speechRecognition // 音声認識
  case camera           // カメラ
  case location         // 位置情報
}

Source: apps/macos/Sources/OpenClawIPC/IPC.swift

ただしこれはmacOSアプリ経由の場合のみです。CLIで直接 node を起動した場合、Node.jsプロセスはTerminalアプリのTCC権限を継承します。Terminalにフルディスクアクセスを許可していれば、OpenClawも同等の権限を持ちます。

まとめ

OpenClawの通信経路を追った結果、以下のことが分かりました。

  • 主要チャネル(Discord、Slack、Telegram、WhatsApp)はすべてローカルから外部への外向き接続で成り立っている。ポート開放やパブリックIPは不要
  • Gateway、Agent Runtime、ツール実行はデフォルトで同一Node.jsプロセス内で動作する。bash実行はNode.jsの child_process によるもの
  • LLMが判断したツール呼び出しは、ユーザー権限でそのまま実行される。これが利便性の源泉であると同時にリスクでもある

通信経路を理解すると、次に気になるのは「この仕組みはどこが攻撃されうるのか」という点です。実際の攻撃事例や防御機構についてはセキュリティモデルの記事でも触れています。

参考リンク

この記事が参考になったら

この記事が役に立ったと思いましたら、よろしければフォローをお願いします。

Xでフォローする

AIエージェントと外部サービスの連携基盤の構築に興味がありましたら、お気軽にご連絡ください。

お問い合わせ

この記事を書いた人

Webプランナー、開発会社の創業メンバーを経て、Fintech・DX・Web3の企画・開発に従事。2021年に独立し、2023年に株式会社hanzochangを設立。現在はAIを活用した業務改善と、自社サービス「指定管理者制度AI」の開発・運用に取り組んでいます。

半澤勇大のXアカウント(新しいタブで開く)

この記事の株式会社hanzochangについて
少し紹介させてください!

この記事に関連する当社のサービス

AIエージェント開発

人の代わりに仕事を進めるAIを、御社の業務にも。

情報収集、原稿や画像の作成、システムへの入力など、複数の作業を実行するAIエージェントを開発します。人が確認する工程も含めて設計し、本番運用まで支援します。

サービスの詳細を見る
メガネをかけたJくんが座禅を組み、背後に多数の手描きワークフローが広がるイラスト。
  • 担当する仕事を設計

    AIに任せる作業と、人が判断する条件を整理。

  • 資料・システムと連携

    社内データやAPIにつなぎ、一連の作業を実装。

  • 評価・本番運用

    出力の精度と実行結果を確かめ、継続的に改善。

業務改善・AI活用について、お気軽にご相談ください。