To use GitHub webhooks to trigger AI coding agents, point a repository webhook at an intake endpoint you own, verify the X-Hub-Signature-256 header against the raw body, reply 202 within GitHub's 10-second window, and push the event onto a queue. A worker then filters the event, maps it to a thread (repo plus PR number) and starts or feeds the agent.
The rest of this guide shows each step in code and how to have your coding agent test the endpoint with Apidog CLI.
Why trigger AI coding agents with GitHub webhooks instead of polling
A webhook trigger is an HTTP call GitHub makes to your server the moment something happens in a repository: a comment, a review, a finished workflow. Your agent sleeps until a delivery arrives, so it spends zero tokens and zero API calls while nothing is going on, and it reacts in seconds instead of on the next poll.
Polling every minute burns rate limit on empty answers and still lags by up to a minute. The long version is in webhooks vs polling.
The DeepSeek Harness team made the same argument in their essay "Exploring Super-agent Design", posted on 1 November 2026. They call these inputs Triggers: the agent watches events and stays quiet otherwise. The essay describes an architecture and a prototype plugin, not a shipped DeepSeek feature. We broke down the full design in how to build a super-agent; this post builds just the trigger layer, for any coding agent.
Which GitHub webhook events should start an agent
Subscribe only to events a human would expect a teammate to act on. Every extra event is a delivery you have to verify, store and filter.
| Event | Action to watch | What the agent does |
|---|---|---|
issue_comment | created | Answers a comment on an issue or a PR conversation, for example "@bot fix the failing test" |
pull_request_review_comment | created | Responds to a comment on a specific line of the diff |
pull_request | opened, synchronize | Reviews a new PR or re-reviews after a push |
workflow_run | completed with conclusion: failure | Reads the failed logs and proposes a fix |
check_run | completed with a failing conclusion | Same as above, for checks reported by apps |
Two details from the GitHub webhook events reference save you debugging time. First, issue_comment fires for comments on both issues and pull requests; a PR comment carries an issue.pull_request object, which is how you tell them apart. Second, workflow_run with action completed fires whether the run passed or failed, so you must check workflow_run.conclusion yourself.
Payloads are capped at 25 MB; GitHub never delivers anything larger.
Verify the X-Hub-Signature-256 signature on the raw body
When you set a secret on the webhook, GitHub signs every delivery with HMAC SHA-256 of the raw request body and sends the hex digest as X-Hub-Signature-256: sha256=<digest>. Your endpoint recomputes the digest and compares the two in constant time.
Three rules from GitHub's validating webhook deliveries guide:
- Hash the raw bytes, not a re-serialized JSON object. Parsing and re-stringifying changes whitespace and breaks the match.
- Compare with a constant-time function such as Node's
crypto.timingSafeEqual, never==. - Use
X-Hub-Signature-256. The olderX-Hub-Signatureheader uses SHA-1 and exists for legacy integrations.
Here's the intake service in Node with 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); The handler does four cheap things and nothing slow. For signature schemes across providers, see webhook signature verification.
Reply 2xx within 10 seconds and queue the agent work
GitHub's webhook best practices say your server should respond with a 2xx within 10 seconds. If it takes longer, GitHub terminates the connection and records the delivery as failed.
An agent run takes minutes, so it can never happen inside the request. Push the event onto a queue (Redis, SQS, a Postgres table, anything durable) and return 202 Accepted right away. A separate worker process pulls from the queue, runs the filters and starts the agent.
Failures matter: GitHub does not automatically redeliver failed deliveries. You can redeliver them by hand or through the API for up to 3 days, so a slow handler silently loses that comment's trigger unless someone notices.
Filter in two stages: fast path in code, slow path with the model
Most deliveries should never reach a model. The DeepSeek essay splits filtering into a fast path written in code and a slow path where an LLM applies policies written as prompts. That split keeps cost tied to real work instead of repository noise.
Fast path (code, runs in microseconds):
- Repo allowlist: only repositories you've opted in.
- Sender rules: ignore the agent's own bot account, or you'll build an infinite loop where it replies to itself.
- Mention mode: start a run only when the comment contains
@your-bot. The essay lists three modes worth copying: respond only to explicit requests, treat as a candidate for the slow path, or ignore.
Slow path (LLM, runs on what survives):
Hand the model the comment text plus a short policy prompt, such as "Is this a request to change code in this PR? Answer yes or no and a one-line reason." Only a "yes" starts the agent. This catches comments like "thanks @bot, looks good", which pass the mention rule but need no work.
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"; } Route follow-ups by thread id so the same agent answers
A thread is the run of consecutive events on one PR. Build thread_id from the repo and the PR or issue number, as the handler above does with acme/api#42. In the essay's design, the worker created for the first event subscribes to its thread, and follow-up events go straight to it without another routing call.
In practice that's a lookup table from thread_id to agent session id. When a delivery arrives for a thread with a live session, append the comment to that session. The agent keeps its context: the diff it read, the test it ran. Without this, three comments on one PR spawn three agents that contradict each other.
The essay adds one more detail worth stealing: triggers that arrive while the router is mid-turn get batched into its next call, so a burst of five comments costs one decision, not five.
Dedupe by X-GitHub-Delivery
Every delivery carries an X-GitHub-Delivery GUID. GitHub's docs note that a redelivery reuses the same value as the original, which makes it the right idempotency key. Store each id (a unique index in your database works) and treat a repeat as a no-op that still returns 2xx, so GitHub doesn't mark it failed.
It also blocks replay attacks: a captured delivery's signature still checks out, but its id is already stored.
Let your coding agent test the trigger endpoint with Apidog CLI
You shouldn't hand-test signature math. Let the coding agent that wrote the intake service also write its tests and run them, with Apidog CLI. Set it up once:
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 Then give Claude Code, Codex or Cursor one [REDACTED PROMPT]
In Apidog project , 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:
- valid X-Hub-Signature-256 -> 202 with trigger_id and a thread_id of "#"
- a second comment on the same PR -> same thread_id
- bad signature -> 401
- 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.
The agent reads each schema, validates its payload, creates the resources and runs them:
bash
apidog cli-schema get endpoint-create
apidog cli-schema validate endpoint-create --file endpoint.json
apidog endpoint create --project --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 --file case-valid-signature.json
apidog run --test-case --project -e -r cli,json
The `cli` reporter prints pass or fail per assertion, and the `json` report lands in `./apidog-reports`, so the agent sees which check broke (a 200 where you expected 401 usually means the raw-body middleware is missing). The same `apidog run` line goes into CI with `-r junit`; see [Apidog CLI in GitHub Actions](http://apidog.com/blog/apidog-cli-github-actions?utm_source=dev.to&utm_medium=wanda&utm_content=n8n-post-automation). For more on the agent setup, read [Apidog CLI in Claude Code](http://apidog.com/blog/apidog-cli-in-claude-code?utm_source=dev.to&utm_medium=wanda&utm_content=n8n-post-automation), and for broader webhook test strategy, [how to test webhooks](http://apidog.com/blog/how-to-test-webhooks?utm_source=dev.to&utm_medium=wanda&utm_content=n8n-post-automation). Apidog's Free plan covers teams of up to 4 users. ## Before you turn the webhook on 1. Secret set; a missing or wrong signature gets 401. 2. 202 in well under 10 seconds, agent work queued. 3. Delivery ids stored with a unique constraint. 4. A job replays failed deliveries from the last 3 days. If you'd rather use GitHub's own runner, [Claude Code GitHub Actions](http://apidog.com/blog/claude-code-github-actions?utm_source=dev.to&utm_medium=wanda&utm_content=n8n-post-automation) is the hosted route; a webhook intake adds the filtering and thread routing. For retries on your side, see [how to design reliable webhooks](http://apidog.com/blog/how-to-design-reliable-webhooks?utm_source=dev.to&utm_medium=wanda&utm_content=n8n-post-automation). ## FAQ **Should I use a repository webhook or a GitHub App?** A repository or organization webhook is enough for one team. A GitHub App gives the agent its own bot identity, finer permissions and installation across many orgs. **What happens if my endpoint is down?** GitHub records the delivery as failed and doesn't retry it. You can redeliver deliveries from the past 3 days in the webhook settings or through the REST API. **How do I stop the agent from replying to itself?** Drop any delivery whose `sender` is your bot account in the fast-path filter, before anything else runs. **Why return 202 instead of 200?** Both count as success for GitHub. 202 Accepted tells anyone reading your logs that the work was queued, not finished. **Is the DeepSeek super-agent design something I can switch on?** No. The essay describes an architecture, and its prototype plugin is not an official DeepSeek product. You build the trigger layer yourself, as shown above. 

