SSD Nodes Learn 🎉 VPS $4.99/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-13

OpenTagをVPSで自ホストする方法

OpenTag v0.9.0をVPSで運用し、SlackやGitHubの@メンションをコーディングエージェントへ届けます。TLS、Webhook署名検証、トークン権限、安全な初期設定を解説します。

エージェントにメンションしたときの OpenTag の動作

OpenTag は、Slack スレッドまたは GitHub issue の @メンションを、所有するマシン上で実行するコーディングエージェントの処理に変換します。誰かが issue に @opentag investigate this とコメントすると、リスナーがプラットフォームイベントを受信して署名を検証し、そのメンションを関連付けられたプロジェクトに照合します。その後、ローカル checkout に対してコーディングエージェントを起動し、同じスレッドに結果を投稿します。

このプロジェクトは MIT ライセンスで提供され、amplifthq/opentag にあります。2026 年 8 月時点で最新のタグ付きリリースは v0.9.0 です。2026 年 7 月 28 日に公開され、npm パッケージとして提供されています。公式のコンテナイメージはないため、固定する対象は npm のバージョンです。以下のすべてのコマンドで、このバージョンを固定します。

GitHub 側の要件があるため、これはラップトップではなく VPS で運用するプロジェクトになります。GitHub は、あらかじめ登録した URL に HTTP リクエストを送信してリポジトリイベントを配信します。そのため、その URL は翌日も同じアドレスで応答できなければなりません。

4 つの構成要素

リスナーはプラットフォームイベントを受信します。リスナーはプラットフォームごとに異なります。GitHub リスナーは、ポート 3050 のパス /github/webhooks にある HTTP エンドポイントです。Slack Events API リスナーは、ポート 3040 の /slack/events で待ち受けます。Slack は Socket Mode でも実行できます。この場合、アプリケーションが外向きの WebSocket 接続を開くため、受信用ポートは不要です。

ディスパッチャーは調整役です。デフォルトではポート 3030 で待ち受け、OPENTAG_DATABASE_PATH で指定されたローカルデータベースファイルに実行状態を保存し、すべての実行について監査証跡を記録します。ホスト外部からこのポートに到達できる状態にしてはいけません。

ランナーはローカルデーモンです。作業をポーリングし、実行を取得してリースを保持します。実行中は、デフォルトで 15 秒ごとにハートビートを送信します。プロジェクトの対象が存在しない場合や、ランナー自身の設定にある許可リスト外の場合、取得した実行を拒否します。このチェックにより、GitHub イベントから、バインドしていないリポジトリをエージェントの対象に指定できないようにします。

エグゼキューターはコーディングエージェントそのものです。OpenTag は ACP (agent client protocol) 経由でエグゼキューターを起動します。ACP は標準入力と標準出力を介して通信する JSON-RPC プロトコルです。そのため、エージェントは OpenTag が指定した作業ディレクトリ内で子プロセスとして実行されます。組み込みの名前には echocodexclaude-codecursoropencodehermesopenclaw があります。まずは echo を使用してください。これはサンプル設定に含まれるエグゼキューターであり、モデルがコードを変更する前に、処理経路全体が動作することを確認できます。

順序は変わりません。プラットフォームイベント、署名検証、実行レコード、取得、エージェント、スレッドへの返信です。

ノートパソコンとトンネルだけでは不十分な理由

GitHub のセットアップガイドでは、ngrok http 3050を実行して、トンネルのホスト名をリポジトリの webhook に貼り付けるよう指示しています。これは最初の10分間は機能します。無料のトンネルホストはプロセスを再起動するたびに変わり、ノートパソコンがスリープすると利用できなくなります。GitHub は古い payload URL を保持して、その URL への送信を繰り返します。そのため、webhook 設定の Recent Deliveries タブには失敗が蓄積しますが、スレッドには何も表示されません。webhook が何もしない状態は、誰も言及していない bot と見分けがつかないため、1週間気付かないこともあります。

VPS を使うと、問題になる2点を解決できます。DNS 名は変わらないため、一度貼り付けた payload URL をそのまま使えます。マシンはスリープしないため、02:00 のコメントにも応答できます。まず、サーバーを適切にセットアップしてください。新しい VPS で最初の10分間に行う設定では、このガイドが前提とするログインユーザーとファイアウォールを説明しています。

Slack は例外です。Socket Mode では Slack 側へ外向きに接続するため、公開 URL は必要ありません。そのため、Slack だけを使う構成では外部に公開せず運用できます。GitHub には同等の仕組みがありません。リポジトリの webhook は受信 HTTP であるため、公開エンドポイントが必要です。公開エンドポイントには TLS (transport layer security) と署名検証が必要です。

Ubuntu で固定したリリースから OpenTag をセルフホストする

OpenTag v0.9.0 には Node.js 22 以降が必要です。Ubuntu 24.04 の標準リポジトリでは Node 18 が提供されるため、NodeSource からインストールします。

curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -v

node -v の出力は v22 以上である必要があります。Node 20 ではインストール時に EBADENGINE 警告が表示され、CLI の起動後に失敗することがあります。

サービス専用のアカウントを作成します。エージェントはこのユーザーの権限で実行されるため、ログイン用アカウントや root は使用しないでください。VPS で最小権限のユーザーを使用する理由では、この分離に手間をかける価値がある理由を説明しています。

sudo adduser --disabled-password --gecos "" opentag
sudo loginctl enable-linger opentag
sudo npm install -g @opentag/cli@0.9.0
command -v opentag

command -v opentag の出力は、/usr/bin/opentag のようなパスである必要があります。Linux では linger の設定が重要です。OpenTag は systemd を通じてバックグラウンドサービスをインストールするため、ユーザーサービスで linger を有効にしないと、SSH セッションを閉じた時点で停止します。

そのユーザーでセットアップを実行します。

sudo -iu opentag opentag setup

セットアップでは、CLI の言語、ローカルの待ち受けアドレス、コーディングエージェント、作業対象のローカルプロジェクト、保存するプラットフォーム認証情報、実行方法の6項目を指定します。待ち受けアドレスは 127.0.0.1 のままにしてください。nginx が TLS を終端してそこへ転送するため、待ち受けポートを外部から到達可能にする必要はありません。GitHub を使用する場合は、リポジトリを owner/repo 形式で指定するか、プルリクエストの作成を許可するか、Webhook ポート(デフォルトは3050)、トークンも指定します。最後にバックグラウンドサービスモードを選択します。既存の設定があり、プロンプトなしでサービスをインストールする場合は、opentag setup --service を使用します。

設定は /home/opentag/.config/opentag/config.json に保存され、実行時状態は /home/opentag/.local/state/opentag に保存されます。セットアップによるファイルの書き込み後、これらのキーを手動で確認してください。

{
  "runnerId": "runner_local",
  "dispatcherUrl": "http://localhost:3030",
  "runnerToken": "...",
  "approvalMode": "ask",
  "repositories": []
}

古い共有 pairingToken よりも、実行単位に限定されたベアラートークンである runnerToken を使用してください。設定ファイルには、Secret 参照に置き換えない限り認証情報が平文で保存されます。Secret 参照を使用すると、起動時に環境変数またはディスク上のファイルから値が読み込まれます。いずれの場合も、このファイルはサーバー上で最も機密性の高いファイルです。モードは600、所有者は opentag とし、git リポジトリ内には置かないでください。より広い観点については、AI エージェントから Secret を分離するを参照してください。

外部公開する前に、インストールを確認します。

sudo -iu opentag opentag doctor
sudo -iu opentag opentag status

opentag doctor はディスパッチャー、バインド、チェックアウト、エグゼキューターを確認します。opentag status は設定と実行時状態を表示し、実行が存在する場合は単一の実行に対象を限定できます。プラットフォームをこのサーバーへ接続する前に、doctor が報告する問題をすべて修正してください。

TLS を前段に置き、2 つのパスだけを公開する

nginx で TLS を終端し、2 つのパスだけを転送します。それ以外はすべて 404 を返すため、ホストを見つけたスキャナーに背後で動作しているサービスの情報を与えません。

/etc/nginx/sites-available/opentag に通常の port 80 用 server block を記述し、以下の 2 つの location を追加します。その後、Certbot に TLS 部分を追加させます。

sudo apt install -y nginx certbot python3-certbot-nginx
sudo ln -s /etc/nginx/sites-available/opentag /etc/nginx/sites-enabled/opentag
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d opentag.example.com

nginx -tsyntax is oktest is successful を出力します。これは、入力ミスによってサイトを停止する reload を防ぐ唯一の手段です。Ubuntu 24.04 の nginx で Certbot を使用するでは、更新処理と ACME(自動証明書管理環境)challenge が失敗する原因を説明しています。完成した block は次のようになります。

server {
    listen 443 ssl;
    server_name opentag.example.com;

    ssl_certificate     /etc/letsencrypt/live/opentag.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/opentag.example.com/privkey.pem;

    client_max_body_size 2m;

    location = /github/webhooks {
        proxy_pass http://127.0.0.1:3050;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    location = /slack/events {
        proxy_pass http://127.0.0.1:3040;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    location / {
        return 404;
    }
}

location = /github/webhooks= は完全一致です。port の後に何も付かない proxy_pass は、元の URI を変更せずにそのまま転送します。= を省略すると、/github/webhooks/ 配下のすべてのパスも転送されます。これは listener に必要な範囲を超えた公開です。

ファイアウォールの範囲は狭く保ちます。

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status

3030、3040、3050 は開放しません。これらがすべてのインターフェースではなく loopback に bind されていることを確認します。

sudo ss -tlnp

すべての OpenTag 行は、127.0.0.1:3030 などになっている必要があります。0.0.0.0:3050 という行は、listener がインターネット全体に公開され、ufw だけがアクセスを阻止していることを意味します。これは、ファイアウォールの設定ミス1つで agent trigger が公開される状態です。ufw ファイアウォールの基本では、この default deny が実際に何を行っているかを説明しています。

2つの確認で、前段の入口を検証できます。curl -I https://opentag.example.com/ は nginx から 404 を返します。これにより、証明書が有効で、catch-all が閉じていることを確認できます。署名のない /slack/events または /github/webhooks へのリクエストは、決して 200 を返してはいけません。

すべての署名を検証する。URL は公開されているため

誰でもペイロードの URL を見つけられます。URL はリポジトリ設定、ブラウザー履歴、チケットに貼り付けたスクリーンショットなどに残ります。署名だけが、本物の GitHub 配信と手動で作成されたリクエストを区別します。

GitHub は webhook secret で各配信に署名し、その結果を x-hub-signature-256 ヘッダーで送信します。OpenTag はそのヘッダーを platforms.github.webhookSecret と照合します。プロジェクトのハードニングノートには、/github/webhooks 上の署名なしの source event を受け入れてはならないと明記されています。Slack は SLACK_SIGNING_SECRET で各リクエストに署名し、タイムスタンプも含めます。そのため、取得された本文を数時間後に再送信することはできません。

この検証を省略することは、小さなリスクではありません。検証されていないエンドポイントは、@opentag を含む手書きの issue_comment ペイロードを受け入れます。その後 OpenTag は、見知らぬ相手の指示に従い、あなたのトークンを使って、あなたの checkout で coding agent を実行します。返信は、偽のペイロードで指定された任意のスレッドに送られます。

OpenTag はさらに 2 つの層を追加します。source delivery は delivery ID で追跡されるため、同じイベントを再配信しても 2 回目の実行は開始されません。runner call は冪等性キーを受け付けるため、1 件を再送信しても、監査イベントを追加せずに成功を返します。

レート制限は設定可能であり、有効にしてください。OPENTAG_RATE_LIMIT_WINDOW_MSOPENTAG_RATE_LIMIT_MAX_REQUESTS がリクエストレートを制限し、OPENTAG_MAX_REQUEST_BODY_BYTES が本文のサイズを制限します。サイズ超過のペイロードは 413 request_body_too_large で拒否されます。OPENTAG_RATE_LIMIT_DISABLED=true はローカル開発用に存在するものであり、公開サーバーで使用してはなりません。同じノートには、もう 1 つのルールもあります。公開 relay URL では HTTPS を使用する必要があり、CLI が平文の HTTP を許可するのは localhost の場合だけです。

bot に実際に必要なトークンのスコープは何ですか?

GitHub では、OpenTag は GitHub App ではなく、きめ細かい権限を設定できる personal access token を使用します。ドキュメントでは App 方式は計画中であり、現在の CLI の標準構成ではないと説明されています。ここで見落とされがちな点があります。bot は、トークンを作成した人間としてコメントを投稿します。すべてのトリアージ返信に自分の名前が表示されても問題ないアカウントでトークンを作成してください。

セットアップガイドに従い、スコープは必要最小限にします。Only select repositories を選択し、リポジトリを 1 つ指定します。Issues: Read and writePull requests: Read and write を付与してください。メンションを読み取り、スレッド内で返信するにはこれで十分です。

不足している権限にも注目してください。コードへの書き込み権限はありません。preparePullRequestBranch が true に設定されていない限り、OpenTag はブランチを push しません。また、コードを書き込むトークンとコメントを書き込むトークンを分離するために、githubApplyToken も用意されています。これらは分けて管理し、読み取りとコメント投稿の経路を数週間運用するまで、書き込みトークンは無効にしておいてください。

避けるべき構成は、All repositories に対して Contents: Read and write を付与したトークンです。これらのリポジトリのいずれかでコメントできる全員が、コミット権限を持つ agent に指示できるようになり、監査証跡にはトークン所有者の操作として記録されます。スコープは、agent が信頼を得た後に、リポジトリ単位で 1 つずつ広げてください。

Slack では、bot のスコープは app_mentions:readchat:writereactions:writechannels:history です。プライベートチャンネルでは、groups:history も必要で、message.groups イベントへのサブスクリプションも必要です。Socket Mode には connections:write を付与した app-level token が必要です。このトークンは xapp- で始まります。channels:history は、bot が追加されているパブリックチャンネルのメッセージ履歴を読み取ります。そのため、bot をすべてのチャンネルに追加するのではなく、必要なチャンネルに追加してください。

問題を最初から最後まで通す

最初に webhook を設定します。リポジトリで Settings、Webhooks、Add webhook の順に開きます。ペイロード URL は https://opentag.example.com/github/webhooks、コンテンツタイプは application/json、Secret はセットアップで生成されたものです。Issue commentsPull request review comments だけを購読し、それ以外は選択しません。

保存すると、GitHub はすぐに ping 配信を送信します。Recent Deliveries を開き、リクエストがサーバーに到達したかを確認します。ここで 502 が返る場合は、nginx がリスナーに接続できなかったことを示します。これは GitHub 側ではなく、ローカル側の問題です。

ここで実際に使います。バグを説明している issue を開き、次のコメントを投稿します。

@opentag triage this. Reproduce the report against the current main branch, then reply with the file and function most likely responsible, plus the test you would write first.

次の順序で動作します。Recent Deliveries に issue_comment 配信が 2xx レスポンスで記録されます。dispatcher が run を記録します。runner がそれを取得し、heartbeat の送信を開始します。executor が checkout を開いて処理します。回答は同じ issue スレッドのコメントとして投稿されます。sudo -iu opentag opentag status には実行中の run が表示されるため、推測せずに進行状況を確認できます。

最初の実運用 run の前に、approvalModeask に設定します。ask モードでは、状態を変更する処理を実行する前に run が一時停止し、担当者の承認を待ちます。auto モードと autonomous モードもあります。これらは、1 か月分の実行記録を確認したリポジトリで、後から使用するのが適切です。

Slack 側では、同じ run がチャンネル内の /bind owner/repo で始まり、その後にメンションが続きます。bot は /help/status/doctor/stop/unbind confirm にも応答します。バインディングを変更できるユーザーを OPENTAG_SLACK_BINDING_ADMIN_USER_IDS で制限します。これは Slack user ID をコンマ区切りで指定するリストです。バインディングは、公開チャンネルとサーバー上の checkout を対応付ける設定です。

最初の経路としては、読み取り専用で書き込みを行わず、回答の評価も容易な triage が適しています。次の段階は review です。ここでは agent が issue ではなく diff にコメントします。セルフホスト型の pull request review agent は、pull request を対象にした同じアーキテクチャです。処理中に agent から自社システムへアクセスさせる場合は、VPS 上の MCP servers を使用します。Web 検索も triage で求められる機能です。agent を自分で運用する SearXNG インスタンスに接続する方法 を使えば、検索を自分で管理するハードウェア上で実行できます。ただし、外部ユーザーのテキストが agent に到達する経路がもう 1 つ増えます。

全員の前でエージェントが誤った場合、どうなりますか?

エージェントは誤ることがあります。問題は、それによって何が起きるかです。

公開 issue への誤った返信は、チームが認識している名前で投稿されるコメントです。GitHub はコメントが投稿された時点で、購読している全員にメールを送信します。コメントを削除しても、メールは取り消せません。Slack 通知も同様です。非公開で正しく動くことではなく、公開の場で誤ることを前提に計画してください。

被害を抑える選択肢は4つあります。どのプロンプトを書くかよりも重要です。

  • ask モードで実行します。エージェントが提案し、人が承認するため、誤った計画でも1クリック分の影響にとどまります。
  • preparePullRequestBranch はデフォルトの false のままにします。不適切な実行で起こり得る最悪の結果を、誤ったブランチではなく誤ったコメントに抑えられます。
  • 最初は1つのリポジトリと1つのチャンネルに限定します。runner は、プロジェクトの対象がローカルの許可リスト外にある実行を拒否します。そのため、未登録のリポジトリからエージェントを起動することはできません。
  • コメント用の token は apply 用の token と分離します。書き込み権限を取り消しても、triage まで停止しないようにします。

Slack には、誤った方向に進んでいる実行を停止する /stop コマンドがあります。すべての実行には監査記録も残ります。そこには、実行を開始した mention とエージェントの動作が記録されます。後からこの記録を読み、どこで誤ったのかを確認できます。

ソーシャル面も設定と同じくらい重要です。bot は、機械が投稿する場所だと利用者が理解し、誤る可能性も認識している1つのチャンネルに配置してください。人が確認したと思われている40人規模のチャンネルで、自信のある誤答を投稿すると、triage で削減できた作業量を上回る影響が生じます。チャンネルの説明には、bot の管理者と出力を確認する担当者を明記してください。

バックアップ、アップグレード、バージョン固定

すべてのデータは2つのパスに保存されます。/home/opentag/.config/opentag/config.json/home/opentag/.local/state/opentag です。前者には認証情報があり、後者には実行履歴とデータベースファイルがあります。両方をモード600でバックアップし、サーバー外に保管してください。これらを失うと、サーバーを再構築するのではなく、トークンとバインディングを再作成することになります。

アップグレードでは、バージョンを更新して再起動します。

sudo npm install -g @opentag/cli@0.9.0
sudo -iu opentag opentag service stop
sudo -iu opentag opentag service start
sudo -iu opentag opentag doctor

@latest を追跡するのではなく、バージョンを固定してください。このソフトウェアは、有効なトークンを使ってリポジトリに対してコーディングエージェントを実行します。そのため、夜間にリリースされたバージョンは、レビューされていない変更に相当します。セキュリティポリシーによるバックポートはなく、修正は最新リリースにのみ含まれます。したがって、バージョンを固定する場合は、変更履歴を確認して、意図的に更新します。v0.9.0 を永久に使い続けるという意味ではありません。2026年7月までの履歴では、毎月複数のリリースが行われています。各バージョン更新の前にリリースノートを読むのが適切です。

FAQ

OpenTag の実行に VPS は必要ですか。ノートパソコンで十分ですか。

Slack だけなら、ノートパソコンで十分です。Socket Mode は外向きの WebSocket 接続を確立するため、受信ポートは必要ありません。GitHub は異なります。リポジトリの webhook は、登録した URL に受信 HTTP で配信されるため、そのアドレスを固定し、スリープ中も応答できる状態にする必要があります。無料アカウントのトンネルホストは再起動のたびに変わります。GitHub は古いアドレスへの送信を続けるため、リポジトリの Recent Deliveries タブに失敗した配信として表示され、スレッドには何も表示されません。固定 DNS 名と証明書を使用できる VPS なら、両方の問題を解消できます。

OpenTag には、どの GitHub 権限が必要ですか。

Only select repositories に限定した fine-grained personal access token を使用し、Issues: Read and writePull requests: Read and write を付与します。これでメンションの読み取りとスレッドへの返信が可能です。コードへの書き込み権限は、OpenTag がブランチを push するように preparePullRequestBranch を true に設定する場合を除き、必要ありません。また、コメント用のトークンとコード書き込み用のトークンを分離できるよう、別の githubApplyToken も用意されています。contents write を付与した全リポジトリ対象のトークンは避けてください。そのトークンの対象リポジトリでコメントできる人なら誰でも、コミットできるエージェントを誘導できるためです。

問題が発生している実行を停止するにはどうすればよいですか。

Slack には、この用途専用の /stop コマンドがあります。サーバー上で opentag status を実行すると実行中の処理が表示され、opentag service stop でデーモンを停止できます。デーモンを停止すると、1 つの実行だけでなくパイプライン全体が終了します。これらの操作が必要にならないようにするには、approvalModeask に設定し、変更を行う前に人の確認を求めるようにします。また、preparePullRequestBranch は false のままにしてください。問題のある実行がブランチを作成せず、コメントを生成するようになります。

webhook が 502 を返すのに、スレッドに何も表示されないのはなぜですか。

502 は OpenTag ではなく nginx が返しています。これは、プロキシがリスナーに接続できなかったことを示します。/var/log/nginx/error.log を実行すると connect() failed (111: Connection refused) while connecting to upstream が表示されます。リスナーが停止しているか、proxy_pass 行で指定されているポートと異なるポートで待ち受けている可能性があります。sudo ss -tlnp を実行し、GitHub 用の 127.0.0.1:3050 と Slack 用の 127.0.0.1:3040 で何らかのプロセスが待ち受けていることを確認します。続いて opentag doctor を実行し、バインディングとエグゼキューターを確認します。

#opentag#ai-agents#slack#github#webhooks#self-hosting