OpenClaw と LaunchAgent のススメ ― AI エージェントのプロセスを「正しく常駐化」する方法

益子 竜与志
益子 竜与志
XThreads
最終更新日:2026年03月26日公開日:2026年03月26日

AI エージェントを Mac で動かしていると、ある日ふと気づくんですよね。「あれ、このプロセス、毎回手動で起動してるの非効率すぎない?」と。OpenClaw という AI エージェント基盤を日常的に使い倒すなかで、プロセスの常駐化と自動起動に正面からぶつかった経験を、pm2 での試行錯誤から macOS LaunchAgent への移行、そして Plugin による完全内部化まで、リアルな失敗談とともにまとめました。

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>

重要な設定項目

キー

意味

RunAtLoad

true にすると、ユーザーログイン時に自動起動

KeepAlive

true にすると、プロセスが死んでも自動で再起動

StandardOutPath

標準出力のログファイルパス

StandardErrorPath

標準エラー出力のログファイルパス

WorkingDirectory

プロセスの作業ディレクトリ

登録と操作はターミナルから行います。

# 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 に統一したのか

pm2 vs LaunchAgent 比較

Node.js 開発者にとっては pm2 のほうが馴染み深いと思います。自分も最初は pm2 で Gateway を管理していました。でも、運用してみるといくつか問題が出てきたんですよね。

比較テーブル

観点

pm2

LaunchAgent

管理場所

Node.js エコシステム(別途インストール)

macOS OS ネイティブ

PC 起動時の自動起動

pm2 startup(sudo が必要な場合あり)

RunAtLoad: true で自動

プロセス死亡時の再起動

✅ 自動

KeepAlive: true で自動

ログ管理

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 で常駐プロセスを完全内部化する

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 の競合解消

registerInteractiveHandler で Gateway の Slack 接続を共有。別の WebSocket 接続は不要

HTTP ルートの同居

registerHttpRoute で Gateway の 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 の状態確認

launchctl list | grep openclaw

ログをリアルタイム監視

tail -f ~/.openclaw/logs/gateway.stdout.log

手動で停止・再起動

launchctl kickstart -k gui/$(id -u)/com.openclaw.gateway

plist の文法チェック

plutil -lint ~/Library/LaunchAgents/com.openclaw.gateway.plist

最近のエラーを確認

log show --predicate 'senderImagePath contains "openclaw"' --last 1h

KeepAlive の注意点

KeepAlive: true は便利ですが、アプリケーションが起動直後にクラッシュするバグがあると、無限再起動ループに陥ります。macOS は短期間に繰り返しクラッシュするプロセスに対してバックオフ(再起動間隔を徐々に延ばす)をかけますが、ログが大量に出力される原因にもなります。本番投入前に、Gateway が安定して起動することを手動で確認しておくことをおすすめします。

まとめ ― シンプルこそ正義

AI エージェントを常駐させるという、一見複雑に見えるタスク。でも最終的にたどり着いた答えは「LaunchAgent 1 本で Gateway を起動し、あとは Plugin に任せる」という、拍子抜けするほどシンプルな構成でした。

振り返ると、pm2 と LaunchAgent を併用していた時期が一番トラブルが多かったです。プロセスマネージャを複数使うと、責任境界が曖昧になって障害の原因特定が難しくなります。これは AI エージェントに限らず、あらゆるサーバーサイドの常駐プロセスに共通する教訓だと思います。

OpenClaw の Plugin アーキテクチャは、この「一元管理」の思想をうまく実現しています。新しい機能を追加したいとき、別のプロセスを立てるのではなく、Plugin として Gateway に同居させる。HTTP ルートも、Slack のインタラクションハンドラも、バックグラウンドサービスも、すべて Gateway プロセスの中に収まります。

「常駐プロセスの管理は、少なければ少ないほど良い。」

この原則を胸に、みなさんも AI エージェントの運用アーキテクチャを見直してみてはどうでしょうか。ちょっとした構成の見直しで、運用の安定性が劇的に変わるかもしれません。

IT/DXプロジェクト推進するPMO・コンサル人材を提供しています

AI利活用×高生産性のリソースで、あらゆるIT/DXプロジェクトを一気通貫支援します

詳しく見る →
AI駆動型ITコンサルティング
Careerバナーconsultingバナー