OpenTagをVPSで自ホストする方法と設定
OpenTag v0.9.0をVPSで動かし、SlackやGitHubの@メンションをコーディングエージェントへ届けます。TLS、Webhook署名検証、トークン権限、安全な初期設定を解説します。
エージェントにメンションしたときの OpenTag の動作
OpenTag は、Slack のスレッドや GitHub の issue にある @メンションを、所有するマシン上で実行するコーディングエージェントの実行に変換します。誰かが issue に @opentag investigate this とコメントすると、リスナーがプラットフォームのイベントを受信して署名を検証し、そのメンションを紐付けられたプロジェクトと照合します。その後、ローカルのチェックアウトに対してコーディングエージェントを起動し、同じスレッドに結果を投稿します。
このプロジェクトは MIT ライセンスで提供され、amplifthq/opentag にあります。2026 年 8 月時点で、最新のタグ付きリリースは 2026 年 7 月 28 日に公開された v0.9.0 で、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 が渡した作業ディレクトリ内で子プロセスとして実行されます。組み込みの名前には echo、codex、claude-code、cursor、opencode、hermes、openclaw があります。まずは echo を使用してください。これはサンプル設定に含まれるエグゼキューターであり、モデルがコードに変更を加える前に、処理経路全体が機能することを確認できます。
順序は変わりません。プラットフォームイベント、署名検証、実行レコード、取得、エージェント、スレッドへの返信です。
ノート PC とトンネルだけでは不十分な理由
GitHub のセットアップガイドでは、ngrok http 3050 を実行して、トンネルのホストをリポジトリの webhook に貼り付けるよう指示しています。これは最初の 10 分間は機能します。無料のトンネルホストはプロセスを再起動するたびに変わり、ノート PC がスリープすると利用できなくなります。GitHub は古い payload 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 -vnode -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 opentagcommand -v opentag は、/usr/bin/opentag のようなパスを出力する必要があります。Linux では linger の設定が重要です。OpenTag は systemd を通じてバックグラウンドサービスをインストールするため、ユーザーサービスで linger が有効でないと、SSH セッションを閉じた時点で停止します。
このユーザーとしてセットアップを実行します。
sudo -iu opentag opentag setupセットアップでは、CLI の言語、ローカルの待ち受けアドレス、コーディングエージェント、作業対象のローカルプロジェクト、保存するプラットフォーム認証情報、実行方法の6項目を指定します。nginx が TLS を終端してこのアドレスへ転送するため、待ち受けアドレスは 127.0.0.1 のままにしてください。これにより、リスナーを外部から到達可能にする必要がありません。GitHub では、owner/repo 形式のリポジトリ、pull request を作成してよいか、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 よりも、runner 用の bearer token である runnerToken を優先してください。Secret 参照に置き換えない限り、設定ファイルには認証情報が平文で保存されます。Secret 参照を使用すると、起動時に環境変数またはディスク上のファイルから値が読み込まれます。いずれの場合も、このファイルはサーバー上で最も機密性の高いファイルです。モードは 600、所有者は opentag とし、git リポジトリ内には決して置かないでください。詳しい説明は AI エージェントから Secret を分離するにあります。
外部公開する前に、インストールを確認します。
sudo -iu opentag opentag doctor
sudo -iu opentag opentag statusopentag 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.comnginx -t は syntax is ok と test 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 statusPorts 3030、3040、3050 は決して開放しません。これらがすべての interface ではなく 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 と照合します。プロジェクトの hardening notes には、ルールが明記されています。/github/webhooks では署名のない source event を受け付けないでください。Slack は SLACK_SIGNING_SECRET で各リクエストに署名し、タイムスタンプも含めます。そのため、取得された body を数時間後に再送しても再生攻撃にはなりません。
これを省略することは、小さなリスクではありません。検証されていない endpoint は、@opentag を含む手書きの issue_comment payload を受け付けます。その結果、OpenTag はあなたの token を使い、あなたの checkout で、見知らぬ相手の指示に従って coding agent を実行します。返信は、偽の payload が指定したスレッドに送られます。
OpenTag はその上に 2 つの層を追加します。source delivery は delivery ID で追跡されるため、同じイベントを再配信しても 2 回目の実行は開始されません。Runner 呼び出しは idempotency key を受け付けるため、同じ呼び出しを再生すると、別の監査イベントを追加せずに成功を返します。
Rate limit は設定可能であり、有効にしてください。OPENTAG_RATE_LIMIT_WINDOW_MS と OPENTAG_RATE_LIMIT_MAX_REQUESTS がリクエストレートを制限し、OPENTAG_MAX_REQUEST_BODY_BYTES が body サイズを制限します。サイズ超過の payload は 413 request_body_too_large で拒否されます。OPENTAG_RATE_LIMIT_DISABLED=true はローカル開発用に存在するものであり、公開サーバーで使用してはいけません。同じ notes には、もう 1 つルールがあります。公開 relay URL では HTTPS を使用する必要があり、CLI が plain HTTP を許可するのは localhost の場合だけです。
Bot が実際に必要とするトークンのスコープは何ですか?
GitHub では、OpenTag は GitHub App ではなく、きめ細かい個人アクセストークンを使用します。ドキュメントでは App 方式は計画中であり、現在の標準的な CLI 構成ではないと説明されています。ここで見落とされがちな点があります。Bot は、トークンを作成した人間としてコメントします。トリアージの返信に自分のアカウント名が表示されても問題ないアカウントで作成してください。
セットアップガイドと同じく、スコープは必要最小限にします。Only select repositories を選択し、リポジトリを 1 つ指定します。Issues: Read and write と Pull requests: Read and write を付与します。メンションを読み取り、スレッド内で返信するにはこれで十分です。
付与されていない権限にも注目してください。コードへの書き込み権限は不要です。preparePullRequestBranch が true に設定されていない限り、OpenTag はブランチを push しません。また、コードを書き込むトークンとコメントを書き込むトークンを分離するために、githubApplyToken も用意されています。両者を分け、読み取りとコメントの経路を数週間運用するまで、書き込みトークンは無効にしておきます。
避けるべき構成は、All repositories に対して Contents: Read and write を付与したトークンです。対象リポジトリのいずれかでコメントできる人は、コミット権限を持つエージェントを誘導できるようになります。監査記録には、その操作をトークンの所有者が行ったように記録されます。スコープは、エージェントが信頼に足る実績を得てから、リポジトリを 1 つずつ追加して広げてください。
Slack では、Bot のスコープは app_mentions:read、chat:write、reactions:write、channels:history です。プライベートチャンネルでは、groups:history に加えて message.groups イベントの購読も必要です。Socket Mode には connections:write を持つアプリレベルのトークンが必要です。このトークンは xapp- で始まります。channels:history は、Bot が追加されているパブリックチャンネルのメッセージ履歴を読み取ります。そのため、Bot をすべてのチャンネルに追加するのではなく、必要なチャンネルにだけ追加してください。
問題を最初から最後まで通す
最初に webhook を設定します。リポジトリで Settings、Webhooks、Add webhook の順に開きます。payload URL は https://opentag.example.com/github/webhooks、content type は application/json、secret はセットアップ時に生成されたものです。Issue comments と Pull request review comments だけを購読し、それ以外は選択しません。
保存すると、GitHub はすぐに ping delivery を送信します。Recent Deliveries を開き、リクエストがサーバーに到達したかどうかを確認します。ここで 502 が返る場合は、nginx が listener に接続できなかったことを示します。これはローカル側の問題であり、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 delivery が 2xx レスポンスで記録されます。dispatcher が run を記録します。runner がそれを取得し、heartbeat の送信を開始します。executor が checkout を開いて処理します。回答が同じ issue スレッドへのコメントとして投稿されます。sudo -iu opentag opentag status では処理中の run を確認できるため、推測せずに進行状況を監視できます。
最初の実行前に approvalMode を ask に設定します。ask モードでは、状態を変更する処理を実行する前に run が一時停止し、人による承認を待ちます。auto モードと autonomous モードもあります。これらは、1 か月分の実行記録を確認したリポジトリで、後から使用するのが適切です。
Slack 側では、同じ run がチャンネル内の /bind owner/repo で始まり、その後に mention を送ります。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 サーバー を使用します。
みんなが見ている前でエージェントが誤った場合、どうなりますか?
エージェントは誤ります。問題は、それによって何が起きるかです。
公開 issue への誤った返信は、チームが認識している名前で投稿されるコメントです。GitHub は投稿された時点で、その issue を購読している全員にメールを送信します。コメントを削除しても、メールは取り消せません。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 には実行履歴とデータベースファイルがあります。両方を mode 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 write と Pull requests: Read and write を付与します。これでメンションの読み取りとスレッドへの返信が可能です。コードへの書き込み権限は、preparePullRequestBranch を true に設定して OpenTag にブランチを push させる場合を除き、必要ありません。また、コメント用のトークンとコード書き込み用のトークンを分離するため、別の githubApplyToken も用意されています。contents write を付与した全リポジトリ対象のトークンは避けてください。それらのリポジトリのいずれかにコメントできる人が、commit 可能な agent を誘導できてしまうためです。
実行が問題を起こしている場合、どう停止すればよいですか?
Slack には、この用途専用の /stop コマンドがあります。サーバー上で opentag status を実行すると実行中の処理が表示され、opentag service stop を実行すると daemon が停止します。daemon を停止すると、1 つの実行だけでなくパイプライン全体が終了します。これらの操作が不要になるようにするには、approvalMode を ask に設定し、変更を加える前に人の確認を求めるようにします。また、preparePullRequestBranch は false のままにしてください。問題のある実行がブランチを作成せず、コメントを生成するだけになります。
Webhook が 502 を返すのに、スレッドに何も表示されないのはなぜですか?
502 は OpenTag ではなく nginx が返しています。これは、プロキシが listener に接続できなかったことを示します。/var/log/nginx/error.log を実行すると connect() failed (111: Connection refused) while connecting to upstream が表示されます。listener が停止しているか、proxy_pass 行に指定されたポートとは別のポートで待ち受けています。sudo ss -tlnp を実行し、GitHub 用の 127.0.0.1:3050 と Slack 用の 127.0.0.1:3040 で何らかのプロセスが待ち受けていることを確認してください。続いて opentag doctor を実行し、binding と executor を確認します。