GitHub Webhookを使ってAIコーディングエージェントを起動する方法
GitHub Webhookを使ってAIコーディングエージェントを起動するには、リポジトリのWebhook送信先を自前の受信用エンドポイントに向け、未加工のリクエストボディ(Raw Body)をもとにX-Hub-Signature-256ヘッダーを検証し、GitHubのタイムアウトである10秒以内に202を返してイベントをキューに投入します。その後、ワーカーがイベントをフィルタリングし、スレッド(リポジトリ名とPR番号)にマッピングした上で、エージェントを起動するか既存のエージェントに追加の指示を送ります。
本ガイドの続きでは、各ステップの実装コードと、コーディングエージェント自身にApidog CLIを使ってエンドポイントをテストさせる方法を解説します。
ポーリングではなくGitHub WebhookでAIコーディングエージェントをトリガーすべき理由
Webhookによるトリガーとは、コメントの投稿、レビュー、ワークフローの完了など、リポジトリ内で何らかのイベントが発生した瞬間にGitHubが自前サーバーに対して行うHTTPリクエストのことです。配信(デリバリー)が届くまでエージェントはスリープしているため、何も起きていない間はトークン消費もAPI呼び出しも完全にゼロで済み、次回のポーリングを待つことなく数秒で即座に反応できます。
1分ごとのポーリングでは、変更がない空のレスポンスに対してもレートリミットを消費し、最大で1分のタイムラグも生じます。詳細な比較はWebhookとポーリングの比較をご覧ください。
DeepSeek Harnessチームも、2026年11月1日に公開した論考「Exploring Super-agent Design(スーパーエージェント設計の探求)」の中で同様の主張を行っています。彼らはこうした入力を「トリガー」と呼んでおり、エージェントはイベントを監視し、それ以外のときは待機状態を維持します。この論考で解説されているのはアーキテクチャとプロトタイププラグインであり、リリース済みのDeepSeek公式機能ではありません。設計全体の解説はスーパーエージェントの構築方法で行っていますが、本記事ではあらゆるコーディングエージェントに対応するトリガー層の実装に焦点を当てます。
どのGitHub Webhookイベントでエージェントを起動すべきか
購読するのは、チームメンバーが対応すべきと考えるようなイベントのみに絞りましょう。余計なイベントを購読するたびに、検証、保存、フィルタリングの処理コストが増加します。
| イベント | 監視対象のアクション | エージェントの処理内容 |
|---|---|---|
issue_comment |
created |
IssueやPRの会話スレッドに投稿されたコメント(例:「@bot テストの失敗を修正して」など)に応答する |
pull_request_review_comment |
created |
差分(diff)の特定の行に付けられたコメントに応答する |
pull_request |
opened, synchronize
|
新規PRのレビュー、またはpush後の再レビューを行う |
workflow_run |
completed(conclusion: failure の場合) |
失敗したログを読み取り、修正案を提案する |
check_run |
失敗の結論を伴うcompleted
|
上記と同様(GitHub App等から報告されるチェック用) |
GitHub Webhookイベントリファレンスに記載されている2つの仕様を押さえておくと、デバッグの手間を大幅に省けます。第1に、issue_commentはIssueとプルリクエストの双方のコメントで発火します。PRのコメントにはissue.pull_requestオブジェクトが含まれているため、これを見て判別します。第2に、アクションがcompletedのworkflow_runは、実行が成功した場合でも失敗した場合でも発火するため、workflow_run.conclusionを自前で確認する必要があります。
ペイロードの上限は25 MBであり、GitHubがこれを超えるサイズのデータを配信することはありません。
未加工のリクエストボディでX-Hub-Signature-256署名を検証する
Webhookにシークレットを設定すると、GitHubは未加工のリクエストボディ(Raw Body)に対してHMAC SHA-256署名を付与し、その16進数ダイジェストをX-Hub-Signature-256: sha256=<digest>として送信します。受信用エンドポイント側ではダイジェストを再計算し、一定時間(Constant-time)比較関数を用いて両者を検証します。
GitHubのWebhook配信の検証ガイドに基づく3つのルールは以下のとおりです:
- 再シリアライズしたJSONオブジェクトではなく、未加工のRawバイト列をハッシュ化すること。パースして再度文字列化すると空白文字などが変化し、ハッシュ値が一致しなくなります。
-
==は絶対に使わず、Node.jsのcrypto.timingSafeEqualのような一定時間比較関数を使用すること。 -
X-Hub-Signature-256を使用すること。旧形式のX-Hub-SignatureヘッダーはSHA-1を使用しており、レガシー連携用として残されているにすぎません。
以下は、Node.jsとExpressを使った受信用サービスの実装例です:
import express from "express";
import crypto from "node:crypto";
const app = express();
const [REDACTED CREDENTIAL];
const seen = new Set(); // use Redis or a DB table in production
function validSignature(raw, header) {
if (!header) return false;
const expected = "sha256=" +
crypto.createHmac("sha256", SECRET).update(raw).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/triggers/github", express.raw({ type: "application/json" }), (req, res) => {
if (!validSignature(req.body, req.get("X-Hub-Signature-256"))) {
return res.status(401).json({ error: "bad signature" });
}
const deliveryId = req.get("X-GitHub-Delivery");
if (seen.has(deliveryId)) {
return res.status(200).json({ duplicate: true, trigger_id: deliveryId });
}
seen.add(deliveryId);
const event = req.get("X-GitHub-Event");
const payload = JSON.parse(req.body);
const prNumber = payload.pull_request?.number ?? payload.issue?.number;
const threadId = `${payload.repository.full_name}#${prNumber}`;
queue.push({ deliveryId, event, threadId, payload }); // your queue client
res.status(202).json({ trigger_id: deliveryId, thread_id: threadId });
});
app.listen(3000);
このハンドラーは負荷の軽い4つの処理のみを行い、時間のかかる処理は一切実行しません。各プロバイダーの署名方式については、Webhook署名の検証を参照してください。
10秒以内に2xxを返し、エージェントのタスクをキューイングする
GitHubのWebhookベストプラクティスでは、サーバーは10秒以内に2xxを返す必要があると定められています。これを超えると、GitHub側で接続が切断され、配信は失敗として記録されます。
エージェントの実行には数分かかるため、HTTPリクエストの処理内で実行することは不可能です。イベントをキュー(Redis、SQS、PostgreSQLのテーブルなど、永続性のあるもの)に投入し、即座に202 Acceptedを返してください。その後、独立したワーカープロセスがキューからイベントを取得し、フィルタリングを実行した上でエージェントを起動します。
失敗を放置してはならない重要な理由があります。それは、GitHubは失敗した配信を自動で再送しないという点です。失敗した配信は手動またはAPI経由で最大3日間は再配信できますが、レスポンスが遅いハンドラーを使っていると、誰かが気付くまでコメントによるトリガーが暗黙のうちに見落とされることになります。
2段階でフィルタリング:コードによるファストパスとモデルによるスローパス
配信されるイベントの大半は、そもそもモデルに渡す必要がありません。DeepSeekの論考では、フィルタリングをコードで記述する「ファストパス(高速処理)」と、プロンプトで定義されたポリシーをLLMが適用する「スローパス(低速・詳細処理)」の2段階に分割しています。この分離により、リポジトリのノイズに余計なコストをかけることなく、実際の作業に対してのみ費用を投じることができます。
ファストパス(コード処理、マイクロ秒単位で実行):
- リポジトリの許可リスト:明示的に対象としたリポジトリのみを許可。
- 送信者ルール:エージェント自身のボットアカウントを無視(これを怠ると、自身に対して返信を繰り返す無限ループが発生します)。
- メンションモード:コメントに
@your-botが含まれている場合のみ実行を開始。論考では参考になる3つのモードが挙げられています。明示的なリクエストのみに応答する、スローパスの候補として扱う、あるいは無視する、のいずれかです。
スローパス(LLM処理、ファストパスを通過したものに対して実行):
コメントのテキストと、「これはこのPRのコード変更を求めるリクエストですか?yesまたはnoと、1行の理由で回答してください」といった短いポリシープロンプトをモデルに渡します。「yes」の場合にのみエージェントを起動します。これにより、メンション条件は満たすものの実際の作業が不要な「ありがとう @bot、良さそうだね」といったコメントを正しく除外できます。
function fastPath({ event, payload }) {
if (payload.sender?.type === "Bot") return "ignore";
if (!ALLOWED_REPOS.has(payload.repository.full_name)) return "ignore";
if (event === "workflow_run")
return payload.workflow_run.conclusion === "failure" ? "run" : "ignore";
const body = payload.comment?.body ?? "";
return body.includes("@your-bot") ? "candidate" : "ignore";
}
同じエージェントが応答できるよう、スレッドIDで後続イベントをルーティングする
スレッドとは、1つのPR上で発生する一連のイベントの流れを指します。上記のハンドラーでacme/api#42としているように、リポジトリ名とPRまたはIssue番号を組み合わせてthread_idを生成します。論考の設計では、初回のイベントで生成されたワーカーがそのスレッドを購読し、後続のイベントは別のルーティング処理を経由することなく直接そのワーカーに送られます。
実際の実装としては、thread_idからエージェントのセッションIDへのルックアップテーブルを用います。稼働中のセッションが存在するスレッドに対して配信が届いた場合、そのセッションにコメントを追加します。これにより、エージェントは過去に読み込んだ差分や実行したテストなどのコンテキストを維持できます。この仕組みがないと、1つのPRに3件のコメントが付いた際に3つのエージェントが生成され、互いに矛盾した動作をしてしまうことになります。
この論考には、取り入れるべき細部設計がもう1つあります。ルーターが処理の実行中(ターン中)に届いたトリガーは次回の呼び出しにバッチ処理としてまとめられるため、5件のコメントが連続して届いた場合でも、判断処理は5回ではなく1回分のコストで済みます。
X-GitHub-Deliveryによる重複排除
すべての配信にはGUIDであるX-GitHub-Deliveryが付与されています。GitHubのドキュメントによると、再配信時にも初回と同一の値が再利用されるため、これが適切な冪等性キー(Idempotency Key)となります。各IDを保存し(データベースの一意性インデックスが適しています)、重複したリクエストは何も処理を行わない(no-op)ようにしつつ2xxを返信することで、GitHub側で失敗と判定されるのを防ぎます。
これにより、リプレイ攻撃を防ぐこともできます。傍受された配信の署名が有効であっても、そのIDはすでに保存されているため処理が弾かれます。
コーディングエージェント自身にApidog CLIでトリガーエンドポイントをテストさせる
署名の計算を手動でテストすべきではありません。受信用サービスを作成したコーディングエージェントに、テストコードの作成と実行まで任せましょう。Apidog CLIを使えばそれが可能です。セットアップは一度行うだけで完了します:
npm install -g apidog-cli@latest
apidog auth login --with-token <YOUR_TOKEN>
npx -y skills add https://apidog.com # pick your agent when prompted
続いて、Claude Code、Codex、Cursorなどにプロンプトを1つ渡します:
In Apidog project <projectId>, create the endpoint POST /triggers/github from the
sample issue_comment payload in fixtures/issue_comment.json. Then create test cases
against the staging environment:
1. valid X-Hub-Signature-256 -> 202 with trigger_id and a thread_id of "<repo>#<number>"
2. a second comment on the same PR -> same thread_id
3. bad signature -> 401
4. same X-GitHub-Delivery sent twice -> second call returns duplicate: true, no new job
Sign the bodies with the staging webhook secret in a pre-request script.
Run them and summarize any failures.
エージェントは各スキーマを読み取り、ペイロードをバリデーションし、リソースを作成してテストを実行します:
apidog cli-schema get endpoint-create
apidog cli-schema validate endpoint-create --file endpoint.json
apidog endpoint create --project <projectId> --file endpoint.json
apidog cli-schema get test-case-create
apidog cli-schema validate test-case-create --file case-valid-signature.json
apidog test-case create --project <projectId> --file case-valid-signature.json
apidog run --test-case <caseId> --project <projectId> -e <stagingEnvId> -r cli,json
cliレポーターはアサーションごとの合否を出力し、jsonレポートは./apidog-reportsに出力されるため、エージェントはどの検証が失敗したかを把握できます(401を期待していた箇所で200が返る場合は、通常Raw Body用ミドルウェアの設定漏れが原因です)。
同じapidog runコマンドは、-r junitを指定することでCIにもそのまま組み込めます。詳細はGitHub ActionsにおけるApidog CLIの活用を参照してください。エージェントのセットアップの詳細についてはClaude CodeでのApidog CLIの活用を、Webhook全体のテスト戦略についてはWebhookのテスト方法をご覧ください。ApidogのFreeプランは最大4名のチームまで無料で利用できます。
Webhookを有効化する前のチェックリスト
- シークレットが設定されており、署名がない場合や誤っている場合に401が返ること。
- 10秒より十分に短い時間で202を返し、エージェントの処理がキューイングされること。
- 配信IDが一意性制約(ユニーク制約)付きで保存されていること。
- 過去3日間の失敗した配信を再実行するジョブが用意されていること。
GitHub自身のランナーを使用したい場合は、ホスト型の選択肢としてClaude Code GitHub Actionsがあります。Webhook受信用エンドポイントを設けることで、フィルタリングやスレッドルーティングの機能を追加できます。自前システム側のリトライ設計については、信頼性の高いWebhookの設計方法を参照してください。
よくある質問(FAQ)
リポジトリWebhookとGitHub Appのどちらを使うべきですか? 単一のチームであれば、リポジトリまたはOrganization単位のWebhookで十分です。GitHub Appを使用すると、エージェント専用のボットIDを持たせることができ、より細かい権限設定や複数Organizationへの導入が可能になります。
エンドポイントがダウンした場合はどうなりますか? GitHubは配信を失敗として記録し、自動的な再試行は行いません。Webhookの設定画面またはREST API経由で、過去3日以内の配信を手動で再送することができます。
エージェントが自分自身に返信し続けるのを防ぐにはどうすればよいですか? 他のどの処理よりも前に、ファストパスフィルターでsenderが自前ボットアカウントである配信を破棄(ドロップ)してください。
なぜ200ではなく202を返すのですか? GitHubにとってはどちらも成功(success)として扱われます。しかし、202 Acceptedを返すことで、ログを確認した人に対して処理が完了したのではなく「キューに受け付けられた」状態であることが明確に伝わります。
DeepSeekのスーパーエージェント設計は、設定をオンにするだけで使える機能ですか? いいえ、違います。あの論考で説明されているのはアーキテクチャであり、プロトタイプのプラグインもDeepSeekの公式製品ではありません。上記で示したように、トリガー層は自前で構築する必要があります。



Top comments (0)