OpenClaw ってそもそも何?
まず前提として、OpenClaw について簡単に紹介しておきます。OpenClaw は、LLM(大規模言語モデル)を中心に据えた AI エージェント基盤です。ざっくり言うと「AI アシスタントを自分のマシン上で常駐させて、Slack や Discord、CLI からいつでも呼び出せるようにするプラットフォーム」ですね。
OpenClaw の基本アーキテクチャ
OpenClaw は以下のような構成で動作します。
コンポーネント | 役割 |
|---|---|
Gateway | 常駐デーモン。LLM との通信、Plugin のロード、外部チャネルとの接続を一元管理 |
Plugin | Gateway に動的にロードされる拡張機能。HTTP ルートの追加やサービスの登録が可能 |
Skill | LLM が参照する手順書。SKILL.md に記述されたガイドラインに従って動作 |
MCP Server | 外部ツールとの接続を担う標準化されたインターフェース |
大事なポイントは、Gateway が常に起動している必要があるということです。Gateway が落ちていると Slack からのメンションも、定期実行の cron タスクも、何も動きません。つまり「Gateway をいかに安定して常駐させるか」がすべての出発点になります。
LaunchAgent って何? macOS ネイティブのプロセス管理
macOS ユーザーなら一度は聞いたことがあるかもしれない LaunchAgent。でもぶっちゃけ、ちゃんと理解して使っている人は少ないんじゃないでしょうか。
LaunchAgent の基本的な仕組み
LaunchAgent は macOS に組み込まれたプロセス管理の仕組みで、~/Library/LaunchAgents/ に plist ファイルを配置するだけで、ログイン時にプロセスを自動起動できます。Linux でいう systemd のユーザーサービスに相当するものですね。
実際の plist ファイルはこんな感じです。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openclaw.gateway</string>
<key>ProgramArguments</key>
<array>
<string>/Users/openclaw/.local/share/mise/installs/node/24.14.0/bin/node</string>
<string>/Users/openclaw/.local/share/mise/installs/node/24.14.0/lib/node_modules/openclaw/dist/cli.mjs</string>
<string>gateway</string>
<string>start</string>
<string>--foreground</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.stderr.log</string>
<key>WorkingDirectory</key>
<string>/Users/openclaw</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>/Users/openclaw</string>
</dict>
</dict>
</plist>重要な設定項目
キー | 意味 |
|---|---|
|
|
|
|
| 標準出力のログファイルパス |
| 標準エラー出力のログファイルパス |
| プロセスの作業ディレクトリ |
登録と操作はターミナルから行います。
# LaunchAgent を登録・起動
launchctl load ~/Library/LaunchAgents/com.openclaw.gateway.plist
# 状態確認
launchctl list | grep openclaw
# 停止・登録解除
launchctl unload ~/Library/LaunchAgents/com.openclaw.gateway.plistこれだけで、Mac を再起動しても OpenClaw Gateway が自動で立ち上がるようになります。シンプルですよね。
pm2 vs LaunchAgent ― なぜ LaunchAgent に統一したのか

Node.js 開発者にとっては pm2 のほうが馴染み深いと思います。自分も最初は pm2 で Gateway を管理していました。でも、運用してみるといくつか問題が出てきたんですよね。
比較テーブル
観点 | pm2 | LaunchAgent |
|---|---|---|
管理場所 | Node.js エコシステム(別途インストール) | macOS OS ネイティブ |
PC 起動時の自動起動 |
|
|
プロセス死亡時の再起動 | ✅ 自動 |
|
ログ管理 | pm2 独自ログ | 任意のパスに stdout/stderr |
依存関係 | npm に依存 | OS 標準機能 |
競合リスク | 二重起動の可能性あり | 単一管理 |
pm2 で起きた実際のトラブル
pm2 を使っていた時期に実際に遭遇したトラブルを紹介します。
トラブル1: 二重起動によるポート競合
pm2 で Gateway を管理しつつ、LaunchAgent でも同じプロセスを登録していた時期がありました。Mac を再起動すると、LaunchAgent が Gateway を起動 → pm2 の pm2 startup も Gateway を起動 → ポート競合で片方がクラッシュ、という事態に。しかもどっちが先に起動するかはタイミング次第なので、再現性がなくて調査が面倒でした。
トラブル2: npm のバージョン管理との相性
mise(旧 rtx)で Node.js のバージョンを管理していると、pm2 がどの Node.js バイナリを使っているのか曖昧になることがあります。LaunchAgent なら ProgramArguments にフルパスを書くので、使用する Node.js バージョンが明示的です。
トラブル3: ログの散在
pm2 のログは ~/.pm2/logs/ に溜まりますが、LaunchAgent のログとは別の場所にあるため、問題発生時にどちらのログを見ればいいのか迷う場面がありました。統一していれば、見るべきログは1箇所で済みます。
結論: 「ひとつの管理者に統一する」が正解
pm2 も LaunchAgent も、単体で使えばどちらも優秀なプロセス管理ツールです。でも両方を同時に使うと、責任の所在が曖昧になって問題が起きます。macOS で動かすなら OS ネイティブの LaunchAgent に統一するのが、結局いちばんシンプルで安定する結論に至りました。
OpenClaw Plugin で常駐プロセスを完全内部化する

LaunchAgent で Gateway を安定して常駐できるようになると、次のステップとして「Gateway の中に追加のサービスを同居させたい」というニーズが出てきます。ここで登場するのが OpenClaw Plugin です。
Plugin の 3 つの登録メソッド
OpenClaw Plugin では、Gateway のライフサイクルにフックして機能を追加できます。主要な 3 つのメソッドを見てみましょう。
registerService ― バックグラウンドサービスの登録
Gateway の起動と同時に走り続けるバックグラウンドサービスを登録します。定期的なポーリングや WebSocket 接続の維持などに使います。
module.exports = {
name: 'approval-bot',
registerService({ logger, config }) {
// Gateway 起動時に呼ばれる
logger.info('approval-bot service started');
// 定期的なヘルスチェックなど
const interval = setInterval(() => {
logger.debug('approval-bot heartbeat');
}, 60000);
// クリーンアップ関数を返す
return () => {
clearInterval(interval);
logger.info('approval-bot service stopped');
};
}
};registerHttpRoute ― HTTP エンドポイントの追加
Gateway の内蔵 HTTP サーバーにカスタムルートを追加します。外部からの Webhook 受信や API エンドポイントの公開に使います。
module.exports = {
name: 'approval-bot',
registerHttpRoute({ router, logger }) {
// 承認リクエストを受け付けるエンドポイント
router.post('/approval-request', async (req, res) => {
const { message, channel, requester } = req.body;
logger.info(`Approval request from ${requester}`);
// Slack にボタン付きメッセージを送信
const result = await postApprovalMessage(channel, message);
res.json({ ok: true, ts: result.ts });
});
// 承認処理
router.post('/approve', async (req, res) => {
const { ts, channel } = req.body;
await updateMessage(channel, ts, '✅ 承認されました');
res.json({ ok: true });
});
// 却下処理
router.post('/reject', async (req, res) => {
const { ts, channel, reason } = req.body;
await updateMessage(channel, ts, `❌ 却下されました: ${reason}`);
res.json({ ok: true });
});
}
};registerInteractiveHandler ― Slack ボタン操作のハンドリング
これが地味にすごい機能です。Slack のインタラクティブコンポーネント(ボタン押下、モーダル送信など)を Gateway 内で直接ハンドリングできます。
module.exports = {
name: 'approval-bot',
registerInteractiveHandler({ logger }) {
return {
// action_id でフィルタリング
actionId: 'approval_action',
async handle({ action, body, ack, respond }) {
await ack();
const approver = body.user.name;
const decision = action.value; // 'approve' | 'reject' | 'pending'
logger.info(`${approver} clicked: ${decision}`);
switch (decision) {
case 'approve':
await respond({
text: `✅ ${approver} が承認しました`,
replace_original: true
});
break;
case 'reject':
await respond({
text: `❌ ${approver} が却下しました`,
replace_original: true
});
break;
case 'pending':
await respond({
text: `⏳ ${approver} が「理由次第」としました`,
replace_original: true
});
break;
}
}
};
}
};この仕組みのおかげで、Slack ボタンの応答処理のために別プロセスを立てる必要がなくなりました。Gateway が持っている Slack 接続をそのまま使えるので、Socket Mode の競合も起きません。
実践: 承認ボット(approval-bot)を作った話

ここからが本題です。実際に OpenClaw Plugin として承認ボット(approval-bot)を作った際の、リアルな失敗と解決の過程を共有します。
最初のアプローチ: Socket Mode で直接接続 → 失敗
最初は「Slack の Socket Mode で直接 Bolt アプリを動かせばいいじゃん」と思って、独立した Node.js プロセスとして承認ボットを書きました。pm2 で管理して、Slack のボタン押下イベントを受け取る設計です。
ところが、ここで大きな問題にぶつかります。
OpenClaw Gateway がすでに Socket Mode 接続を掴んでいる。
Slack の Socket Mode は、1 つのアプリトークンに対して基本的に 1 つの WebSocket 接続しか維持できません(複数接続は可能ですが、イベントがどちらに飛ぶか制御しにくい)。Gateway が Slack チャネルの監視のために Socket Mode を使っている状態で、別プロセスからも Socket Mode 接続を張ると、イベントの配信先が不安定になります。
具体的には、ボタン押下のイベントが Gateway に飛んでしまい、承認ボット側では受け取れないという事象が発生しました。
第2のアプローチ: HTTP API サーバー方式 → 動いたけど...
Socket Mode がダメなら HTTP でやろう、ということで、承認ボットを独立した Express サーバーとして実装し、Gateway の LLM からは HTTP 経由でリクエストを送る構成にしました。
# pm2 で承認ボットを起動
pm2 start approval-bot.js --name approval-bot
# Gateway(LaunchAgent で管理)
launchctl load ~/Library/LaunchAgents/com.openclaw.gateway.plistこれは技術的には動きました。が、運用上の問題が出ます。
pm2 + LaunchAgent の二重管理による混乱。
Mac を再起動すると、LaunchAgent が Gateway を起動し、pm2 の startup スクリプトが approval-bot を起動します。一見うまくいきそうですが、以下の問題が次々と発生しました。
問題 | 原因 |
|---|---|
ポート競合 | pm2 が approval-bot を複数起動してしまうケース |
起動順序の不定 | Gateway より先に approval-bot が起動すると、依存する API が未準備 |
ログの分散 | pm2 ログと LaunchAgent ログが別々の場所に出力される |
障害切り分けの難しさ | どちらのプロセスマネージャの問題か特定しにくい |
最終形: OpenClaw Plugin として内部化
試行錯誤の末、たどり着いたのが「approval-bot を OpenClaw Plugin として Gateway に内部化する」という方法です。
Plugin にすることで、以下のメリットが得られました。
メリット | 詳細 |
|---|---|
プロセス管理の統一 | LaunchAgent で Gateway 1 本を管理するだけ。Plugin は Gateway と一緒に起動・停止 |
Socket Mode の競合解消 |
|
HTTP ルートの同居 |
|
ログの一元化 | Gateway のロガーを使うので、すべてのログが同じファイルに出力される |
起動順序の保証 | Gateway が起動してから Plugin がロードされるので、依存関係の問題なし |
承認フロー全体像
最終的な承認フローはこのようなシーケンスになります。

ポイントは、LLM(OpenClaw のメインエージェント)が承認の必要性を判断し、Plugin の HTTP エンドポイントを呼び出すところです。Plugin は Slack にボタン付きメッセージを投稿し、承認者がボタンを押すと registerInteractiveHandler 経由で応答が返ってきます。すべてが Gateway プロセス内で完結しているのがミソですね。
アーキテクチャの最終形
すべてを整理すると、最終的なアーキテクチャは驚くほどシンプルになりました。
管理するものは LaunchAgent の plist 1 つだけ
macOS 起動
└─ LaunchAgent (com.openclaw.gateway)
└─ OpenClaw Gateway (--foreground)
├─ Slack 接続 (Socket Mode)
├─ HTTP サーバー (:3456)
├─ MCP Server 群
├─ Skill 群
└─ Plugin 群
├─ approval-bot
│ ├─ registerService (初期化)
│ ├─ registerHttpRoute (/approval-request, /approve, /reject)
│ └─ registerInteractiveHandler (ボタン応答)
├─ other-plugin-a
└─ other-plugin-bこれが「LaunchAgent 1本 → Gateway → Plugin が自動ロード」というアーキテクチャです。
このアーキテクチャの良いところ
外部に依存するプロセスマネージャが一切不要です。pm2 も supervisor も Docker も使いません。macOS の標準機能だけで、AI エージェントとその拡張機能をすべて常駐化できます。
もう一つ大事なのは、Plugin の追加・削除が Gateway の再起動だけで完了するという点です。新しい Plugin を作ったら所定のディレクトリに配置して openclaw gateway restart するだけ。pm2 の設定変更も、新しい plist の追加も要りません。
LaunchAgent 運用の Tips
最後に、LaunchAgent を実際に運用するうえで役立つ Tips をいくつか紹介します。
ログローテーション
LaunchAgent 自体にはログローテーション機能がありません。放っておくとログファイルが肥大化します。macOS の newsyslog を使うか、logrotate を Homebrew でインストールして対応するのがおすすめです。
# newsyslog.conf に追加
# /etc/newsyslog.d/openclaw.conf
/Users/openclaw/.openclaw/logs/gateway.stdout.log openclaw:staff 644 5 1024 * J
/Users/openclaw/.openclaw/logs/gateway.stderr.log openclaw:staff 644 5 1024 * J環境変数の注意点
LaunchAgent から起動されるプロセスは、通常のターミナルセッションとは異なる環境で動きます。.zshrc や .bashrc で設定した環境変数は読み込まれません。必要な環境変数は plist の EnvironmentVariables に明示的に記述するか、アプリケーション側で .env ファイルから読み込むようにしましょう。
デバッグ時のコマンド
やりたいこと | コマンド |
|---|---|
LaunchAgent の状態確認 |
|
ログをリアルタイム監視 |
|
手動で停止・再起動 |
|
plist の文法チェック |
|
最近のエラーを確認 |
|
KeepAlive の注意点
KeepAlive: true は便利ですが、アプリケーションが起動直後にクラッシュするバグがあると、無限再起動ループに陥ります。macOS は短期間に繰り返しクラッシュするプロセスに対してバックオフ(再起動間隔を徐々に延ばす)をかけますが、ログが大量に出力される原因にもなります。本番投入前に、Gateway が安定して起動することを手動で確認しておくことをおすすめします。
まとめ ― シンプルこそ正義
AI エージェントを常駐させるという、一見複雑に見えるタスク。でも最終的にたどり着いた答えは「LaunchAgent 1 本で Gateway を起動し、あとは Plugin に任せる」という、拍子抜けするほどシンプルな構成でした。
振り返ると、pm2 と LaunchAgent を併用していた時期が一番トラブルが多かったです。プロセスマネージャを複数使うと、責任境界が曖昧になって障害の原因特定が難しくなります。これは AI エージェントに限らず、あらゆるサーバーサイドの常駐プロセスに共通する教訓だと思います。
OpenClaw の Plugin アーキテクチャは、この「一元管理」の思想をうまく実現しています。新しい機能を追加したいとき、別のプロセスを立てるのではなく、Plugin として Gateway に同居させる。HTTP ルートも、Slack のインタラクションハンドラも、バックグラウンドサービスも、すべて Gateway プロセスの中に収まります。
「常駐プロセスの管理は、少なければ少ないほど良い。」
この原則を胸に、みなさんも AI エージェントの運用アーキテクチャを見直してみてはどうでしょうか。ちょっとした構成の見直しで、運用の安定性が劇的に変わるかもしれません。















