Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 GitHub API 자동화하기: 안전한 설계와 예제

Claude Code로 안전한 GitHub API 자동화를 만드는 방법. 권한, 페이지네이션, rate limit, Webhook 예제를 다룹니다.

Claude Code로 GitHub API 자동화하기: 안전한 설계와 예제

GitHub API를 사용하면 Issue, Pull Request, Release, workflow 상태, repository 메타데이터, Webhook을 코드로 읽고 갱신할 수 있습니다. Claude Code와 함께 쓰면 매일 반복하던 저장소 점검을 자동화하기 좋습니다. 새 Issue 분류, 오래된 PR 리포트, release note 초안, daily repository health report 같은 작업이 대표적입니다.

하지만 GitHub API 자동화는 잘못 만들면 빠르게 위험해집니다. Personal Access Token을 로그에 남기거나, classic token에 너무 넓은 repo 권한을 주거나, 첫 페이지만 읽고 전체를 처리했다고 착각하거나, rate limit에 걸렸는데 무한 재시도하거나, Webhook 서명을 검증하지 않는 문제가 자주 발생합니다. Claude Code에는 “동작하는 스크립트”가 아니라 “token을 숨기고, 권한을 줄이고, 실패해도 안전한 스크립트”를 요청해야 합니다.

구현 중에는 공식 문서를 기준으로 확인하세요. 기본은 GitHub REST API docs, 제한 정책은 REST API rate limit docs, Webhook 수신은 Webhook delivery validation, 복잡한 집계는 GitHub GraphQL API를 함께 봅니다. 팀 workflow에 넣을 때는 Git workflow 가이드GitHub Actions 고급 가이드도 이어서 읽으면 좋습니다.

안전한 흐름부터 잡기

첫 버전은 반드시 읽기 전용으로 시작하는 편이 좋습니다. 스크립트가 어떤 Issue와 PR을 대상으로 보는지 확인하고, 그다음 dry-run으로 “수정 예정 내역”만 출력합니다. 실제 label 추가, comment 작성, close 같은 변경은 명시적인 승인 flag 뒤에 둡니다.

flowchart LR
  A["Claude Code에 목표와 금지사항 전달"] --> B["최소 권한으로 읽기"]
  B --> C["페이지네이션과 rate limit 처리"]
  C --> D["dry-run 결과 확인"]
  D --> E["명시적 승인 후 쓰기"]
  E --> F["schedule 또는 Webhook 운영"]

REST API는 endpoint가 리소스에 직접 대응합니다. open PR 목록을 가져오거나, Issue에 label을 붙이거나, Release를 만드는 작업에 적합합니다. URL과 HTTP method가 명확해서 Claude Code가 작은 단위로 구현하기도 쉽습니다. GraphQL API는 여러 repository, PR author, review 상태, label, milestone처럼 관계가 많은 데이터를 한 번에 뽑는 report에 강합니다. 처음에는 REST로 안전한 기본기를 만들고, report가 복잡해질 때 GraphQL을 검토하면 충분합니다.

기준REST APIGraphQL API
적합한 일Issue, PR, Release 단일 작업여러 리소스를 묶은 집계
학습 난도URL과 method 중심이라 낮음query와 schema 설계가 필요
Claude Code 요청endpoint별로 작게 요청필요한 field를 먼저 정의
주의점pagination 누락, 권한 부족query 비대화, 비용 추정

Token과 권한을 먼저 줄이기

GitHub token은 비밀번호와 같습니다. 코드에 붙여 넣지 말고, README 예제에 쓰지 말고, console.log(process.env)처럼 환경 변수 전체를 출력하지 마세요. 이 글의 예제는 모두 GITHUB_TOKEN에서 token을 읽고 token 값을 출력하지 않습니다.

가능하면 fine-grained personal access token을 사용하고 repository와 permission을 좁힙니다. Issue triage bot은 Issues: Read and write가 필요할 수 있지만, stale PR reporter는 Pull requests: Read-only로 충분한 경우가 많습니다. release note 초안은 Contents: ReadMetadata: Read부터 시작할 수 있습니다. classic token의 repo scope는 편하지만 너무 넓으므로 짧은 검증용으로만 쓰는 것이 안전합니다.

GitHub Actions에서 실행할 때도 workflow permissions를 명시합니다. daily report는 write 권한이 없어야 합니다. label을 붙이는 job만 필요한 write 권한을 갖게 합니다. Claude Code에 workflow를 만들게 할 때는 YAML보다 먼저 “기능, endpoint, 필요한 permission” 표를 요구하세요.

GitHub REST API를 사용하는 Node.js 스크립트를 만들어 주세요.
요구사항:
- token은 process.env.GITHUB_TOKEN에서 읽습니다.
- token이나 Authorization header 전체를 출력하지 않습니다.
- owner/repo는 환경 변수에서 읽습니다.
- 첫 버전은 읽기 전용이며 Issue, PR, Release를 갱신하지 않습니다.
- fetch를 사용하고 status code, pagination, rate-limit header를 처리합니다.
- 필요한 GitHub 권한을 README 형식으로 짧게 설명합니다.

복사해서 실행하는 읽기 전용 스크립트

다음 예제는 Node.js 18 이상에서 open issue를 조회합니다. token을 파일에 쓰지 않고, repository도 환경 변수로 받습니다.

export GITHUB_TOKEN="github_pat_xxx"
export GITHUB_OWNER="octocat"
export GITHUB_REPO="Hello-World"
node scripts/list-open-issues.mjs
// scripts/list-open-issues.mjs
const { GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO } = process.env;

if (!GITHUB_TOKEN || !GITHUB_OWNER || !GITHUB_REPO) {
  throw new Error("Set GITHUB_TOKEN, GITHUB_OWNER, and GITHUB_REPO.");
}

const apiVersion = "2026-03-10";

async function github(path, options = {}) {
  const response = await fetch(`https://api.github.com${path}`, {
    ...options,
    headers: {
      Accept: "application/vnd.github+json",
      Authorization: `Bearer ${GITHUB_TOKEN}`,
      "X-GitHub-Api-Version": apiVersion,
      "User-Agent": "claudecodelab-safe-github-api-example",
      ...(options.headers ?? {}),
    },
  });

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`GitHub API ${response.status}: ${body.slice(0, 500)}`);
  }

  return response.json();
}

const issues = await github(
  `/repos/${encodeURIComponent(GITHUB_OWNER)}/${encodeURIComponent(GITHUB_REPO)}/issues?state=open&per_page=10`,
);

const rows = issues
  .filter((issue) => !issue.pull_request)
  .map((issue) => ({
    number: issue.number,
    title: issue.title,
    labels: issue.labels.map((label) => label.name).join(", "),
    updated: issue.updated_at,
  }));

console.table(rows);

이 코드가 제대로 동작하면 다음 단계는 “어떤 label을 붙일지 제안만 출력”하는 dry-run입니다. 바로 Issue를 close하거나 comment를 쓰지 마세요. APPLY=true 같은 명확한 flag와 최대 처리 건수를 두면 사고를 크게 줄일 수 있습니다.

Pagination과 rate limit은 필수입니다

GitHub REST 목록 endpoint는 대부분 pagination을 사용합니다. per_page=100을 붙여도 100개를 넘으면 다음 페이지를 읽어야 합니다. release note generator가 첫 페이지만 읽으면 오래된 merged PR이 빠지고, stale PR reporter가 첫 페이지만 읽으면 실제보다 건강해 보이는 report가 나옵니다.

rate limit도 무시하면 안 됩니다. 403 또는 429가 나왔다고 즉시 무한 재시도하면 CI 시간과 API quota를 낭비합니다. retry-afterx-ratelimit-reset header를 읽고, 합리적인 시간만 기다리며, 최대 재시도 횟수를 둡니다.

// scripts/github-pages.mjs
const token = process.env.GITHUB_TOKEN;
if (!token) throw new Error("Set GITHUB_TOKEN.");

const apiBase = "https://api.github.com";
const apiVersion = "2026-03-10";

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function defaultHeaders() {
  return {
    Accept: "application/vnd.github+json",
    Authorization: `Bearer ${token}`,
    "X-GitHub-Api-Version": apiVersion,
    "User-Agent": "claudecodelab-pagination-example",
  };
}

function parseNextLink(linkHeader) {
  if (!linkHeader) return null;
  for (const part of linkHeader.split(",")) {
    const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/);
    if (match && match[2] === "next") return match[1];
  }
  return null;
}

async function githubRequest(url, options = {}, attempt = 0) {
  const response = await fetch(url, {
    ...options,
    headers: {
      ...defaultHeaders(),
      ...(options.headers ?? {}),
    },
  });

  if ((response.status === 403 || response.status === 429) && attempt < 2) {
    const retryAfterSeconds = Number(response.headers.get("retry-after") ?? "0");
    const resetSeconds = Number(response.headers.get("x-ratelimit-reset") ?? "0");
    const resetDelayMs = resetSeconds > 0 ? resetSeconds * 1000 - Date.now() : 0;
    const waitMs = Math.max(retryAfterSeconds * 1000, resetDelayMs, 0);

    if (waitMs > 0 && waitMs <= 10 * 60 * 1000) {
      await sleep(waitMs + 1000);
      return githubRequest(url, options, attempt + 1);
    }
  }

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`GitHub API ${response.status}: ${body.slice(0, 500)}`);
  }

  return {
    data: await response.json(),
    nextUrl: parseNextLink(response.headers.get("link")),
  };
}

export async function paginate(path) {
  const items = [];
  let url = path.startsWith("http") ? path : `${apiBase}${path}`;

  while (url) {
    const page = await githubRequest(url);
    if (!Array.isArray(page.data)) {
      throw new Error("paginate() expected an array response.");
    }
    items.push(...page.data);
    url = page.nextUrl;
  }

  return items;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const owner = process.env.GITHUB_OWNER;
  const repo = process.env.GITHUB_REPO;
  if (!owner || !repo) throw new Error("Set GITHUB_OWNER and GITHUB_REPO.");

  const pulls = await paginate(
    `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls?state=open&per_page=100`,
  );
  console.table(pulls.map((pr) => ({ number: pr.number, title: pr.title, updated: pr.updated_at })));
}

공통 helper를 만들었다면 Claude Code에 api.github.com을 직접 호출하는 코드를 찾아 모두 helper 경유로 바꾸게 하세요. 이 한 번의 정리가 pagination 누락을 줄입니다.

Webhook은 서명 검증과 idempotency가 필요합니다

Webhook은 polling 대신 이벤트에 반응하게 해 줍니다. PR이 열리면 label 후보를 만들고, Issue가 생성되면 triage queue에 넣고, Release가 공개되면 다른 시스템에 알릴 수 있습니다. 하지만 endpoint가 공개되어 있다면 GitHub가 아닌 요청도 들어올 수 있습니다. 그래서 x-hub-signature-256 검증이 필수입니다.

또한 idempotency, 즉 같은 delivery가 두 번 와도 부작용이 중복되지 않는 성질이 필요합니다. GitHub는 같은 delivery를 다시 보낼 수 있으므로 x-github-delivery 값을 저장하고 이미 처리한 ID는 무시해야 합니다.

npm install express
export GITHUB_WEBHOOK_SECRET="your-webhook-secret"
node webhook-server.mjs
// webhook-server.mjs
import crypto from "node:crypto";
import express from "express";

const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) throw new Error("Set GITHUB_WEBHOOK_SECRET.");

const app = express();
const seenDeliveries = new Set();

function verifySignature(payloadBuffer, signatureHeader) {
  if (!signatureHeader) return false;

  const expected = `sha256=${crypto
    .createHmac("sha256", secret)
    .update(payloadBuffer)
    .digest("hex")}`;

  const actual = Buffer.from(signatureHeader, "utf8");
  const expectedBuffer = Buffer.from(expected, "utf8");

  return actual.length === expectedBuffer.length && crypto.timingSafeEqual(actual, expectedBuffer);
}

app.post("/github/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.get("x-hub-signature-256");
  if (!verifySignature(req.body, signature)) {
    return res.status(401).send("invalid signature");
  }

  const deliveryId = req.get("x-github-delivery");
  if (!deliveryId) return res.status(400).send("missing delivery id");

  if (seenDeliveries.has(deliveryId)) {
    return res.status(202).send("duplicate ignored");
  }
  seenDeliveries.add(deliveryId);

  const event = req.get("x-github-event");
  const payload = JSON.parse(req.body.toString("utf8"));

  console.log(
    JSON.stringify({
      event,
      deliveryId,
      repository: payload.repository?.full_name,
      action: payload.action,
    }),
  );

  return res.status(202).send("accepted");
});

app.listen(process.env.PORT ?? 3000, () => {
  console.log("Listening for GitHub webhooks.");
});

운영에서는 seenDeliveries를 DB나 Redis로 바꾸고, handler 안에서 바로 대량 수정하지 말고 queue에 넣는 편이 안전합니다. Claude Code에 이 제약을 명확히 전달하세요.

실전 use case

Issue triage bot은 새 Issue의 재현 절차, 환경 정보, 관련 label 후보를 확인합니다. 첫 버전은 자동 comment 대신 daily report로만 제안합니다.

Stale PR reporter는 30일 이상 업데이트가 없는 PR, review request가 오래 남은 PR, CI 실패 후 멈춘 PR을 찾습니다. 자동 close가 아니라 report 중심으로 시작해야 합니다.

Release note generator는 두 tag 사이에 merge된 PR을 모아 Added, Fixed, Changed로 분류합니다. 복잡한 author와 review 정보를 함께 보고 싶으면 GraphQL을 고려합니다.

Daily repository health report는 open issue 수, 오래된 PR, 실패 workflow, Dependabot alert, 최근 release, review backlog를 한 장으로 정리합니다. 이 주제는 Claude Code workflow automationreview workflow checklist와도 잘 이어집니다.

피해야 할 실패

classic token을 로그에 남기는 실수는 가장 먼저 막아야 합니다. console.log(process.env), 전체 request dump, CI debug log를 조심하세요. 권한도 작게 시작해야 합니다. 읽기 전용 report에 write 권한이 있으면 이미 과합니다.

pagination 누락은 조용히 품질을 망칩니다. rate-limit loop는 CI와 API quota를 낭비합니다. Webhook 서명 미검증은 외부 JSON을 믿는 사고입니다. 대량 close, 대량 relabel, release rewrite 같은 destructive bulk edit는 dry-run, 건수 제한, audit log, rollback 계획 없이는 실행하지 마세요.

Claude Code의 역할

Claude Code는 API client, pagination helper, 테스트, README, GitHub Actions schedule, review checklist를 빠르게 만드는 데 적합합니다. production token 발급, 권한 승인, 파괴적 변경의 최종 판단은 사람이 가져야 합니다. 이 경계를 CLAUDE.md에 적어 두면 팀 전체의 사용 방식이 안정됩니다.

ClaudeCodeLab은 팀의 CLAUDE.md, 권한 설계, GitHub Actions, review gate, Webhook 운영 규칙을 함께 정리할 수 있습니다. 팀 도입은 Claude Code 교육 및 컨설팅에서 시작하고, 개인 연습은 무료 cheatsheet템플릿 자료를 활용하세요.

정리

GitHub API와 Claude Code는 repository 운영을 크게 줄여 주지만, token, scope, pagination, rate limit, Webhook 검증, idempotency를 빠뜨리면 위험합니다. 읽기 전용, dry-run, 제한된 쓰기 순서로 발전시키는 것이 현실적인 안전책입니다.

Masa가 직접 확인한 결과, 이 글의 읽기 전용 스크립트, pagination helper, Webhook 서명 검증 서버는 복사해서 검토하기 쉬운 구조로 정리할 수 있었습니다. 실제 운영에서는 bot이 바로 Issue를 닫는 방식보다 daily report로 먼저 보여 주고, 신뢰가 생긴 뒤 label 쓰기를 추가하는 방식이 훨씬 안전했습니다.

#Claude Code #GitHub API #자동화 #CI/CD #개발 생산성
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.