GitHub 웹훅을 사용해 AI 코딩 에이전트를 트리거하려면, 레포지토리 웹훅이 직접 구축한 수신 엔드포인트를 가리키도록 설정하고, 원시(raw) 본문과 X-Hub-Signature-256 헤더를 대조하여 검증한 뒤, GitHub의 10초 제한 시간 내에 202 응답을 반환하고 이벤트를 큐에 넣어야 합니다. 그런 다음 워커가 이벤트를 필터링하고, 이를 특정 스레드(레포지토리 및 PR 번호)에 매핑하여 에이전트를 시작하거나 추가 작업을 전달합니다.
본 가이드의 나머지 부분에서는 각 단계를 코드로 구현하는 방법과 코딩 에이전트가 Apidog CLI를 사용해 해당 엔드포인트를 직접 테스트하도록 만드는 방법을 살펴봅니다.
폴링 대신 GitHub 웹훅으로 AI 코딩 에이전트를 트리거해야 하는 이유
웹훅 트리거는 댓글 작성, 코드 리뷰, 워크플로 완료 등 레포지토리에서 이벤트가 발생하는 즉시 GitHub이 여러분의 서버로 보내는 HTTP 요청입니다. 이벤트가 전달될 때까지 에이전트는 대기 상태를 유지하므로, 아무 일도 일어나지 않는 동안 토큰이나 API 호출을 전혀 소모하지 않으며 다음 폴링 주기까지 기다릴 필요 없이 몇 초 만에 즉각 반응합니다.
1분마다 폴링하는 방식은 빈 응답을 받느라 API 호출 한도(rate limit)를 낭비할 뿐만 아니라 최대 1분의 지연이 발생합니다. 자세한 비교 내용은 웹훅 vs 폴링에서 확인하실 수 있습니다.
DeepSeek Harness 팀 역시 2026년 11월 1일에 발표한 에세이 “슈퍼 에이전트 설계 탐구(Exploring Super-agent Design)”에서 동일한 주장을 펼쳤습니다. 이들은 이러한 입력을 '트리거(Trigger)'라고 부르며, 에이전트가 이벤트를 모니터링하다가 이벤트가 없을 때는 유휴 상태를 유지하도록 설계했습니다. 해당 에세이는 아키텍처와 프로토타입 플러그인을 설명한 것으로, 공식 출시된 DeepSeek의 기능은 아닙니다. 전체적인 설계 분석은 슈퍼 에이전트 구축 방법에서 다루었으며, 이번 글에서는 어떤 코딩 에이전트에도 적용할 수 있는 트리거 계층 구현에만 집중합니다.

어떤 GitHub 웹훅 이벤트가 에이전트를 실행해야 할까
팀원이 직접 조치를 취할 법한 이벤트만 구독하세요. 불필요한 이벤트를 추가할 때마다 검증하고 저장하고 필터링해야 할 전달 건수(delivery)만 늘어납니다.
| 이벤트 | 감지할 액션 | 에이전트의 역할 |
|---|---|---|
issue_comment |
created |
이슈나 PR 대화창의 댓글에 응답 (예: “@bot 실패한 테스트 수정해 줘”) |
pull_request_review_comment |
created |
코드 diff의 특정 라인에 달린 댓글에 응답 |
pull_request |
opened, synchronize |
새로운 PR을 리뷰하거나 추가 푸시 후 다시 리뷰 수행 |
workflow_run |
completed with conclusion: failure |
실패한 로그를 확인하고 수정 사항 제안 |
check_run |
completed with a failing conclusion |
앱이 보고한 체크(check)에 대해 위와 동일하게 동작 |
GitHub 웹훅 이벤트 레퍼런스에서 알아두면 디버깅 시간을 아낄 수 있는 두 가지 세부 사항이 있습니다. 첫째, issue_comment는 이슈와 풀 리퀘스트(PR) 양쪽의 댓글 모두에서 발생합니다. PR 댓글에는 issue.pull_request 객체가 포함되어 있으므로 이를 통해 둘을 구분할 수 있습니다. 둘째, completed 액션의 workflow_run은 실행이 성공했든 실패했든 상관없이 발생하므로 workflow_run.conclusion을 직접 확인해야 합니다.
페이로드는 최대 25MB로 제한되며, GitHub은 이보다 큰 페이로드를 전달하지 않습니다.
원시 본문(Raw Body)을 기반으로 X-Hub-Signature-256 서명 검증하기
웹훅에 비밀 키(secret)를 설정하면 GitHub은 원본 요청 본문의 HMAC SHA-256 해시값으로 모든 전달 건을 서명한 뒤, 16진수 다이제스트를 X-Hub-Signature-256: sha256=<digest> 형태로 전송합니다. 수신 엔드포인트는 이 다이제스트를 다시 계산하여 두 값을 상수 시간(constant time) 비교 함수로 대조해야 합니다.
GitHub의 웹훅 전달 검증 가이드에서 제시하는 세 가지 원칙은 다음과 같습니다.
- 다시 직렬화된 JSON 객체가 아닌 원본 바이트(raw bytes)를 해싱해야 합니다. 파싱 후 다시 문자열로 변환하면 공백이 달라져 서명이 일치하지 않게 됩니다.
==연산자 대신 Node의crypto.timingSafeEqual과 같은 상수 시간 비교 함수를 사용하세요.X-Hub-Signature-256을 사용하세요. 기존X-Hub-Signature헤더는 SHA-1을 사용하며 레거시 연동을 위해 남아 있는 것입니다.
Express 기반 Node 환경에서의 수신 서비스 구현 예시는 다음과 같습니다.
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.GITHUB_WEBHOOK_SECRET;
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);
이 핸들러는 가벼운 작업 네 가지만 처리하며 속도를 늦추는 작업은 일절 수행하지 않습니다. 다양한 제공업체의 서명 방식에 대한 자세한 내용은 웹훅 서명 검증을 참고하세요.
10초 이내에 2xx 응답을 반환하고 에이전트 작업 큐에 등록하기
GitHub의 웹훅 모범 사례에 따르면 서버는 10초 이내에 2xx 상태 코드로 응답해야 합니다. 이보다 오래 걸리면 GitHub은 연결을 종료하고 해당 전달 건을 실패로 기록합니다.
에이전트 실행에는 수분이 걸리므로 웹훅 요청 처리 과정에서 직접 실행해서는 안 됩니다. 이벤트를 큐(Redis, SQS, Postgres 테이블 등 영속성을 보장하는 저장소)에 넣고 즉시 202 Accepted를 반환하세요. 이후 별도의 워커 프로세스가 큐에서 이벤트를 가져와 필터를 거친 뒤 에이전트를 실행합니다.
실패 처리는 매우 중요합니다. GitHub은 실패한 전달 건을 자동으로 재전송하지 않습니다. 최대 3일 동안 웹훅을 수동으로 또는 API를 통해 재전송할 수 있으므로, 핸들러 응답이 느리면 누군가 알아채기 전까지 해당 댓글 트리거가 소리 없이 유실됩니다.
2단계 필터링: 코드 기반 빠른 경로와 모델 기반 심층 경로
대부분의 웹훅 이벤트는 모델에 도달할 필요조차 없습니다. DeepSeek 에세이에서는 필터링을 코드로 작성된 빠른 경로(Fast Path)와 프롬프트로 정의된 정책을 LLM이 적용하는 심층 경로(Slow Path)의 두 단계로 나눕니다. 이러한 구분을 통해 레포지토리의 노이즈로 인한 비용 낭비를 막고 실제 유의미한 작업에만 비용을 집중할 수 있습니다.

빠른 경로 (코드 기반, 마이크로초 단위 실행):
- 레포지토리 허용 목록: 사전에 지정한 레포지토리만 처리합니다.
- 발신자 규칙: 에이전트 자체 봇 계정은 무시합니다. 그렇지 않으면 에이전트가 자신의 댓글에 스스로 응답하는 무한 루프가 발생합니다.
- 멘션 모드: 댓글에
@your-bot이 포함되어 있을 때만 실행을 시작합니다. 에세이에서는 참고할 만한 세 가지 모드로 명시적 요청에만 응답하기, 심층 경로 후보로 전달하기, 무시하기를 제시합니다.
심층 경로 (LLM 기반, 빠른 경로를 통과한 대상만 처리):
댓글 본문과 함께 “이 댓글이 현재 PR의 코드 수정을 요청하는 내용인가요? 예/아니오와 한 줄 이유로 답하세요.”와 같은 짧은 정책 프롬프트를 모델에 전달합니다. 오직 “예”인 경우에만 에이전트를 실행합니다. 이를 통해 “고마워 @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로 후속 이벤트를 라우팅하여 동일한 에이전트가 응답하도록 설정
스레드는 하나의 PR에서 발생하는 일련의 연속적인 이벤트를 의미합니다. 앞서 작성한 핸들러의 acme/api#42처럼 레포지토리와 PR 또는 이슈 번호를 결합하여 thread_id를 생성합니다. 에세이의 설계에 따르면 첫 번째 이벤트로 생성된 워커가 해당 스레드를 구독하며, 이후의 후속 이벤트는 추가 라우팅 호출 없이 해당 워커로 곧바로 전달됩니다.
실제 구현에서는 thread_id를 에이전트 세션 ID에 매핑하는 조회 테이블(lookup table)을 사용합니다. 활성 세션이 있는 스레드로 이벤트가 수신되면 해당 세션에 댓글을 추가합니다. 덕분에 에이전트는 이미 확인한 코드 diff나 실행했던 테스트 결과 같은 컨텍스트를 그대로 유지할 수 있습니다. 이러한 처리가 없다면 한 PR에 댓글이 3개 달렸을 때 서로 상충되는 답변을 내놓는 에이전트 3개가 각각 실행될 것입니다.
에세이에는 또 한 가지 유용한 팁이 있습니다. 라우터가 작업을 처리하는 도중에 도착한 트리거들을 다음 호출 시 배치(batch)로 묶어 처리함으로써, 댓글 5개가 한꺼번에 쏟아져도 5번이 아닌 1번의 판단 비용만 발생하도록 만듭니다.
X-GitHub-Delivery를 사용한 중복 제거
모든 전달 건에는 고유한 X-GitHub-Delivery GUID가 포함되어 있습니다. GitHub 공식 문서에 명시되어 있듯 재전송(redelivery) 시에도 원본과 동일한 값이 유지되므로 멱등성(idempotency) 키로 활용하기에 적합합니다. 각 ID를 저장해 두고(데이터베이스의 Unique 인덱스 활용 권장), 중복 요청이 들어오면 아무 작업도 수행하지 않되 GitHub에서 실패로 처리하지 않도록 2xx 응답을 반환합니다.
이 방식은 재생 공격(replay attack)도 방지합니다. 가로챈 전달 건의 서명이 유효하더라도 해당 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에 다음과 같은 프롬프트를 전달합니다.
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 활용하기, 포괄적인 웹훅 테스트 전략은 웹훅 테스트 방법을 참고하시기 바랍니다. Apidog 무료 플랜은 최대 4인 팀까지 지원합니다.
웹훅을 활성화하기 전 체크리스트
- 비밀 키가 설정되어 서명이 없거나 올바르지 않은 경우 401을 반환하는가?
- 10초보다 훨씬 빠른 시간 내에 202를 반환하고 에이전트 작업을 큐에 넣는가?
- 전달 ID(Delivery ID)가 Unique 제약 조건과 함께 저장되는가?
- 최근 3일 동안 실패한 전달 건을 재시도하는 작업(Job)이 마련되어 있는가?
GitHub 자체 러너를 선호한다면 Claude Code GitHub Actions와 같은 호스팅 방식을 이용할 수 있으며, 여기에 웹훅 수신단을 추가하여 필터링과 스레드 라우팅을 구성할 수 있습니다. 수신 측 재시도 처리에 관한 내용은 신뢰할 수 있는 웹훅 설계 방법을 참고하세요.
자주 묻는 질문 (FAQ)
레포지토리 웹훅과 GitHub App 중 무엇을 사용해야 하나요? 단일 팀의 용도로는 레포지토리나 조직(Organization) 웹훅으로도 충분합니다. 반면 GitHub App을 사용하면 에이전트에 독립적인 봇 ID를 부여할 수 있고 세부적인 권한 제어 및 여러 조직에 걸친 설치가 가능합니다.
엔드포인트가 다운되면 어떻게 되나요? GitHub은 해당 전달을 실패로 기록하며 재시도하지 않습니다. 웹훅 설정 페이지나 REST API를 통해 최근 3일 이내의 전달 건을 수동으로 재전송할 수 있습니다.
에이전트가 자신에게 스스로 응답하지 않게 하려면 어떻게 해야 하나요? 다른 로직이 실행되기 전에 빠른 경로 필터에서 sender가 봇 계정인 모든 전달 건을 걸러내어 폐기(drop)하세요.
200 대신 202를 반환하는 이유는 무엇인가요? GitHub 입장에서는 둘 다 성공으로 간주됩니다. 하지만 202 Accepted는 작업이 완료된 것이 아니라 큐에 정상 등록되었음을 로그를 확인하는 사람에게 명확하게 전달합니다.
DeepSeek 슈퍼 에이전트 설계를 바로 켜서 쓸 수 있는 기능이 있나요? 아닙니다. 해당 에세이는 아키텍처를 제시한 것이며 프로토타입 플러그인은 DeepSeek의 공식 제품이 아닙니다. 위에서 설명한 것처럼 트리거 계층은 직접 구축해야 합니다.
