Otomatisasi GitHub API dengan Claude Code secara aman
Panduan aman memakai GitHub API dengan Claude Code: permission, pagination, rate limit, webhook, dan contoh Node.
GitHub API memungkinkan kita membaca dan memperbarui issue, pull request, release, metadata repository, status workflow, dan webhook lewat kode. Dengan Claude Code, pekerjaan repository yang berulang bisa dibuat lebih rapi: triage issue baru, laporan PR yang lama tidak bergerak, draf release note, dan daily repository health report.
Namun automasi GitHub API yang buruk juga bisa berbahaya. Kesalahan yang sering terjadi adalah token muncul di log, classic token diberi scope repo yang terlalu luas, script hanya membaca halaman pertama, retry berjalan tanpa batas saat rate limit, webhook diterima tanpa validasi signature, atau Claude Code diminta melakukan bulk edit yang destruktif. Targetnya bukan sekadar script yang jalan, tetapi automasi yang aman, terbatas, dan mudah direview.
Gunakan dokumentasi resmi sebagai rujukan: GitHub REST API docs, REST API rate limits, validasi webhook, dan GitHub GraphQL API. Untuk menyambungkannya ke workflow tim, baca juga Git workflow guide dan advanced GitHub Actions guide.
Mulai dari pola aman
Versi pertama sebaiknya read-only. Script hanya menampilkan issue atau PR yang menjadi target, sehingga kita bisa mengecek permission, pagination, dan format output tanpa mengubah data. Setelah itu tambahkan dry-run: program menampilkan label, komentar, atau assignment yang akan dibuat, tetapi belum menerapkannya. Write operation baru boleh berjalan dengan flag eksplisit seperti APPLY=true dan batas jumlah item.
flowchart LR
A["Berikan tujuan dan batasan ke Claude Code"] --> B["Baca dengan permission minimum"]
B --> C["Tangani pagination dan rate limit"]
C --> D["Tampilkan dry-run"]
D --> E["Tulis setelah approval eksplisit"]
E --> F["Jalankan lewat schedule atau Webhook"]
REST API cocok untuk memulai karena endpoint-nya jelas: list PR, tambah label, buat release, atau baca workflow run. Claude Code bisa mengimplementasikannya per endpoint. GraphQL API lebih cocok saat laporan perlu menggabungkan repository, PR, author, review, label, dan milestone dalam satu query.
| Kriteria | REST API | GraphQL API |
|---|---|---|
| Cocok untuk | Aksi langsung pada resource | Laporan lintas resource |
| Kesulitan | Lebih mudah, URL dan HTTP method | Lebih sulit, perlu desain query |
| Prompt ke Claude Code | Endpoint per endpoint | Tentukan field dulu |
| Risiko utama | Lupa pagination atau permission | Query terlalu besar |
Token dan permission harus sempit
GitHub token sama sensitifnya dengan password. Jangan tulis di kode, README, screenshot, test snapshot, atau log. Contoh di artikel ini membaca GITHUB_TOKEN dari environment variable dan tidak pernah mencetak nilainya.
Gunakan fine-grained personal access token jika bisa. Batasi repository dan permission. Issue triage bot mungkin perlu Issues: Read and write; stale PR reporter biasanya cukup Pull requests: Read-only; release note generator bisa mulai dari Contents: Read dan Metadata: Read. Scope repo pada classic token memang praktis, tetapi terlalu luas untuk automasi harian.
Di GitHub Actions, tulis permissions secara eksplisit. Daily report tidak perlu write access. Job yang menambah label hanya perlu permission write yang relevan. Sebelum meminta Claude Code menulis YAML, minta tabel fungsi, endpoint, dan permission yang dibutuhkan.
Buat script Node.js yang memakai GitHub REST API.
Syarat:
- Token dibaca dari process.env.GITHUB_TOKEN.
- Jangan mencetak token atau Authorization header penuh.
- owner/repo dibaca dari environment variables.
- Versi pertama read-only.
- Pakai fetch dan tangani status code, pagination, serta rate-limit headers.
- Tambahkan bagian README singkat berisi permission GitHub yang diperlukan.
Script read-only yang bisa dijalankan
Contoh berikut berjalan di Node.js 18 atau lebih baru dan menampilkan open issue tanpa mengubah 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);
Langkah berikutnya bukan langsung menutup issue. Minta script menampilkan kandidat label, draft komentar, atau assignee. Write operation sebaiknya hanya aktif saat flag eksplisit diberikan dan jumlah perubahan dibatasi.
Pagination dan rate limit
Banyak endpoint list di GitHub memakai pagination. per_page=100 bukan berarti semua item, hanya satu halaman. Release note generator yang membaca halaman pertama saja akan melewatkan PR lama. Stale PR reporter juga bisa memberi gambaran yang salah.
Rate limit perlu ditangani dari awal. Retry langsung pada setiap 403 atau 429 hanya membuang waktu CI dan quota. Baca retry-after dan x-ratelimit-reset, tunggu dalam batas wajar, lalu gagal setelah jumlah percobaan tertentu.
// 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 })));
}
Setelah helper ini dibuat, minta Claude Code mencari api.github.com dan mengganti direct call dengan helper yang sama.
Webhook perlu signature dan idempotency
Webhook berguna ketika automasi harus bereaksi pada event. PR baru bisa memicu saran label, issue baru bisa masuk triage queue, release baru bisa mengirim notifikasi. Karena endpoint biasanya publik, validasi x-hub-signature-256 wajib dilakukan sebelum payload dipercaya.
Idempotency berarti delivery yang sama tidak boleh membuat efek ganda. Simpan x-github-delivery sebelum menjalankan side effect. Contoh ini memakai Set untuk demo; produksi sebaiknya memakai database atau Redis.
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.");
});
Saat memperluas handler dengan Claude Code, larang parsing payload sebelum signature valid, jangan jalankan perubahan destruktif langsung di request HTTP, dan abaikan delivery yang sudah pernah diproses.
Use case konkret
Issue triage bot memeriksa issue baru, melihat apakah langkah reproduksi hilang, lalu menyarankan label seperti bug, question, atau needs-repro. Versi pertama sebaiknya hanya membuat laporan.
Stale PR reporter menemukan PR yang 30 hari tidak berubah, review request yang lama menggantung, atau CI yang gagal tanpa tindak lanjut. Versi aman hanya melapor, tidak menutup PR.
Release note generator mengumpulkan PR yang merged di antara dua tag dan mengelompokkan berdasarkan label. REST cukup untuk awal; GraphQL membantu jika perlu author, reviewer, dan milestone.
Daily repository health report menggabungkan jumlah open issue, PR lama, workflow gagal, Dependabot alert, release terbaru, dan review backlog. Laporan yang baik menonjolkan tiga aksi terpenting hari itu. Topik ini cocok dengan Claude Code workflow automation dan review workflow checklist.
Kegagalan yang harus dihindari
Kegagalan pertama adalah token muncul di log. Hindari console.log(process.env), dump request penuh, dan CI debug log yang berisi header. Cek dengan rg "GITHUB_TOKEN|Authorization|process.env".
Kegagalan kedua adalah permission terlalu luas. Report read-only tidak perlu write access. Kegagalan ketiga adalah pagination yang hilang. Keempat adalah loop rate limit. Kelima adalah webhook tanpa validasi signature. Keenam adalah bulk edit destruktif tanpa dry-run, batas jumlah, audit log, dan rencana rollback.
Peran Claude Code
Claude Code cocok untuk membuat API client, helper pagination, test, README, GitHub Actions schedule, dan checklist review. Keputusan tentang production token, approval permission, dan perubahan destruktif tetap harus dipegang manusia. Tulis aturan ini di CLAUDE.md agar tim memakai standar yang sama.
ClaudeCodeLab membantu tim menyusun CLAUDE.md, permission design, GitHub Actions, review gate, dan operasi Webhook. Untuk rollout tim, mulai dari Claude Code training and consultation. Untuk latihan personal, gunakan free cheatsheet dan templates.
Hasil
GitHub API dan Claude Code bisa mengurangi maintenance repository, tetapi hanya jika token, permission, pagination, rate limit, Webhook verification, dan idempotency didesain sejak awal. Urutan paling aman adalah read-only, dry-run, lalu limited write.
Hasil verifikasi Masa: script read-only, helper pagination, dan server validasi signature Webhook di artikel ini disusun agar bisa dicopy dan direview. Dalam repository nyata, memulai dari daily report sebelum menambah write label jauh lebih dipercaya dibanding bot yang langsung menutup issue sejak hari pertama.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.