GitHub API with Octokit skill for Codex
Paginate to the end, stay under every rate limit. Codex writes GitHub API scripts that choose the narrowest token, paginate every list instead of stopping at 30 items, and pick REST or GraphQL by data shape. It configures Octokit throttling for primary and secondary limits, uses ETags for cheap polling, pins the API version header, and runs bulk edits as a dry run you approve before any issue or branch changes.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- github-api Installs to ~/.agents/skills/github-api/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install github-api The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project github-api Installs to .agents/skills/github-api/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/github-api.zipunzip -o github-api.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) github-api PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $github-api, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Calls the GitHub REST and GraphQL APIs from scripts and services with Octokit, gh api or plain HTTP - token selection, API version headers, pagination, primary and secondary rate limits, conditional requests, retries, search limits and safe bulk writes. Use when automating issues, pull requests, releases or repository settings, exporting data across many repos, hitting 403 or 429 rate limit errors, choosing between REST and GraphQL, or writing a GitHub integration script in Node or Python.
Use it when
- Automating issues, pull requests, releases or repo settings with code
- Collecting data across many repositories or an organisation
- Scripts hit 403 or 429 rate limit responses
- Deciding between REST, GraphQL and gh api for a task
Use something else when
- You are building an installable integration that reacts to webhooks: use github-apps
- You want repo chores to run as workflows: use github-actions-automation
How to check the result
Item counts for one repo match the GitHub UI, the job ends with rate limit headroom logged, and bulk changes were applied only after a confirmed dry run.
The mistake it prevents
Reading only the first page of results, since GitHub returns 30 items by default and silently omits the rest unless you paginate.
Done well means: the script uses the narrowest token that works, paginates to the end instead of stopping at 30 items, stays under primary and secondary rate limits by design, retries only what is safe to retry, and runs every bulk write as a dry run first, with the user confirming before anything is created, closed, merged or deleted.
Before writing code
- Read
AGENTS.mdand check for an existing client (octokit,@octokit/rest, PyGithub,ghscripts). Reuse its auth and retry setup. - Pick the token:
| Context | Token |
|---|---|
| One-off script on your machine | gh auth token or a fine-grained PAT scoped to the repos needed |
| GitHub Actions in the same repo | GITHUB_TOKEN with permissions: set in the workflow |
| Service acting for an organisation, many repos | GitHub App installation token: use $github-apps |
| Acting as a specific user in your web app | OAuth or GitHub App user access token |
Prefer fine-grained PATs over classic ones. Never print tokens or put them in URLs. 3. Decide REST or GraphQL:
| Need | Use |
|---|---|
| Single resource reads and writes, webhooks-style objects, simple lists | REST |
| Nested data across many objects in one round trip (PRs with reviews and checks) | GraphQL |
| Projects (v2), discussions | GraphQL (REST coverage is partial) |
| Quick ad hoc query from a shell | gh api |
- Writes to repositories the user does not own, mass edits, and anything destructive (closing issues in bulk, deleting branches or releases, changing settings) need explicit confirmation with a dry-run list shown first.
API calls need network access; in a Codex sandbox, ask for approval. gh reuses the user’s
login, so confirm which account it is (gh auth status) before writes.
Octokit setup (Node)
import { Octokit } from "octokit"; // bundles REST, GraphQL, pagination, retry and throttling
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
userAgent: "acme-repo-audit/1.0",
throttle: {
onRateLimit: (retryAfter, options, o, retryCount) => {
o.log.warn(`Rate limit on ${options.method} ${options.url}, retry in ${retryAfter}s`);
return retryCount < 2; // retry twice, then fail
},
onSecondaryRateLimit: (retryAfter, options, o) => {
o.log.warn(`Secondary limit on ${options.method} ${options.url}`);
return true; // the plugin waits retryAfter before retrying
},
},
});
Pin the REST version with the X-GitHub-Api-Version header. Requests without it use
2022-11-28; 2026-03-10 is the first calendar version with breaking changes. Upgrade
deliberately and re-test.
Pagination
// All open issues across pages (default page size is 30; max per_page is 100)
const issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
owner, repo, state: "open", per_page: 100,
});
// Stream large result sets without holding everything in memory
for await (const { data: repos } of octokit.paginate.iterator(octokit.rest.repos.listForOrg, { org, per_page: 100 })) {
for (const r of repos) await audit(r);
}
gh api --paginate "repos/OWNER/REPO/pulls?state=closed&per_page=100" --jq '.[] | [.number, .merged_at] | @tsv'
gh api graphql --paginate -f query='
query($endCursor: String) { repository(owner: "OWNER", name: "REPO") {
pullRequests(first: 100, after: $endCursor, states: MERGED) {
nodes { number title mergedAt } pageInfo { hasNextPage endCursor } } } }'
listForRepoon issues also returns pull requests; filter out items withpull_requestset.- GraphQL:
first/lastmax 100; paginate withpageInfo { hasNextPage endCursor }oroctokit.graphql.paginate. Nested connections each need their own pagination. - Search API returns at most 1,000 results per query; split by date ranges
(
created:2025-01-01..2025-03-31) to cover more.
Rate limits
| Limit | Value (github.com) |
|---|---|
| REST primary, user token or PAT | 5,000 requests per hour |
GITHUB_TOKEN in Actions | 1,000 requests per hour per repository |
| Unauthenticated | 60 per hour per IP |
| GraphQL primary | 5,000 points per hour (cost depends on query shape) |
| Search | 30 requests per minute (code search lower) |
| Secondary | No more than 100 concurrent requests; per-minute point caps; content-creating requests about 80 per minute and 500 per hour |
- Read
x-ratelimit-remainingandx-ratelimit-reset(REST) or queryrateLimit { cost remaining resetAt }(GraphQL) and slow down before hitting zero. - Secondary limits return 403 or 429, sometimes with
retry-after. Wait at least that long; without the header, wait a minute and back off exponentially. - Make write-heavy jobs serial with a small delay (about one second between mutations).
- Conditional requests: send
If-None-Matchwith the storedETag; a 304 response does not count against the primary limit. Use this for polling.
Safe bulk writes
const targets = (await octokit.paginate(octokit.rest.issues.listForRepo, { owner, repo, state: "open", labels: "stale", per_page: 100 }))
.filter((i) => !i.pull_request);
console.table(targets.map((i) => ({ number: i.number, title: i.title, updated: i.updated_at })));
if (!process.argv.includes("--apply")) process.exit(0); // dry run by default
for (const i of targets) {
await octokit.rest.issues.update({ owner, repo, issue_number: i.number, state: "closed", state_reason: "not_planned" });
await new Promise((r) => setTimeout(r, 1000));
}
Show the dry-run table to the user and run with --apply only after they confirm. Log each
change so it can be reversed.
Python
Use httpx or requests with a session, or PyGithub for object-style access:
import httpx, os
s = httpx.Client(base_url="https://api.github.com", headers={
"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
})
def all_issues(owner: str, repo: str):
url = f"/repos/{owner}/{repo}/issues?state=all&per_page=100"
while url:
r = s.get(url)
r.raise_for_status()
yield from r.json()
url = r.links.get("next", {}).get("url") # follow the Link header, never build page numbers
Gotchas
- 404 on a private repo usually means the token lacks access, not that the repo is missing.
- Fine-grained PATs need explicit repository selection and per-permission grants; organisation owners may need to approve them.
- Creating content in parallel (issues, comments, PRs) trips secondary limits fast. Serialise.
- Timestamps are ISO 8601 UTC;
sincefilters useupdated_at, notcreated_at. - Large diffs and files: the contents API caps file size; use the Git Data API (blobs) or
Accept: application/vnd.github.rawfor big files. - GitHub Enterprise Server: base URL is
https://HOST/api/v3(GraphQL at/api/graphql) and available endpoints follow the server version.
Verification
- Compare item counts with the web UI or
ghfor one repo before running across many. - Log remaining rate limit at start and end; the job should finish with headroom.
- Run bulk jobs against a test repository or with the dry-run flag first.
- For GraphQL, check
rateLimit.costof the main query; split it if a single call costs more than a few dozen points.
Report template
**Goal:** close issues labelled stale with no activity for 90 days in acme/api
**Auth:** fine-grained PAT, issues:write on acme/api only
**Method:** REST issues.listForRepo (paginated, 412 items) -> filter -> dry run (37 matches)
**Applied:** 37 issues closed as not_planned after user confirmation; log in out/closed.csv
**Rate limit:** 4,563 remaining at end; no secondary limit hits What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "GitHub API with Octokit"
short_description: "Paginate to the end, stay under every rate limit"
default_prompt: "$github-api Export every merged PR from our org's repos in 2025 to a CSV" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.