midudev/autoskills · error · Error
GitHub 403 rate limit exceeded
Error message
GitHub 403 rate limit exceeded${resetSuffix} for ${url}. Set GITHUB_TOKEN or GH_TOKEN to increase the limit. What it means
ghFetch wraps GitHub API calls in the sync-skills script. A 403 status combined with x-ratelimit-remaining: 0 means GitHub's rate limit is exhausted for the current identity (unauthenticated IP or the configured token). The script throws a dedicated error including the reset time and the offending URL, since retrying before reset is pointless.
Solutions
- Set GITHUB_TOKEN (or GH_TOKEN) to a valid personal access token before running the sync script.
- Wait until the reset timestamp in the error message, then re-run the sync.
- Verify an existing GITHUB_TOKEN is valid and unexpired (a bad token can fall back to anonymous limits).
- Reduce sync frequency or scope (fewer repos/skills per run) to stay under the limit.
Example fix
// before $ node scripts/sync-skills.mjs // 403, rate limit exhausted // after $ GITHUB_TOKEN=ghp_xxx node scripts/sync-skills.mjs
Defensive patterns
Strategy: retry
Validate before calling
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
if (!token) throw new Error("Set GITHUB_TOKEN before running the sync script");
// optionally pre-check budget
const rl = await fetch("https://api.github.com/rate_limit", { headers: { Authorization: `Bearer ${token}` } }).then(r => r.json());
if (rl.resources.core.remaining < 20) throw new Error(`Low GitHub quota: ${rl.resources.core.remaining}`); Try / catch
import { setTimeout as sleep } from "node:timers/promises";
async function ghFetchWithRetry(url, tries = 2) {
try {
return await ghFetch(url);
} catch (e) {
const m = e.message.match(/resets (.+?)\)/);
if (m && tries > 0) {
await sleep(Math.max(0, new Date(m[1]).getTime() - Date.now()) + 1000);
return ghFetchWithRetry(url, tries - 1);
} else throw e;
}
} Prevention
- Export GITHUB_TOKEN in the environment (CI secrets, direnv) before syncing.
- Pre-check the rate_limit endpoint for bulk syncs and pace requests.
- Monitor token expiry and rotate PATs proactively.
- Schedule syncs infrequently and cache previous results to cut request volume.
When it happens
Trigger: Any ghFetch(url) call during the sync (listing repo trees, fetching skill files via resolveRepoHead etc.) returning 403 with the rate-limit-remaining header at 0 — typical for large syncs without GITHUB_TOKEN set, or a token that has hit its limit.
Common situations: Bulk skill sync from CI shared IPs without a token (60 req/hr unauthenticated); an expired or revoked GITHUB_TOKEN silently treated as anonymous; many rapid sync runs in succession.
Related errors
- GitHub rate limit exceeded
- GitHub for
- git tree truncated for
- ⚠️ No se pudo crear la release en GitHub (¿tienes gh…
- Tarball fetch failed
AI-assisted analysis of midudev/autoskills@0ec725320d (2026-09-15).
Data as JSON: /api/errors/5f5da7a704ef5f13.
Report an issue: GitHub.
Appendix: source
Thrown at packages/autoskills/scripts/sync-skills.mjs:157
}
return byRepo;
}
// ── GitHub helpers ───────────────────────────────────────────
async function ghFetch(url) {
const headers = {
"User-Agent": "autoskills-sync",
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
if (GITHUB_TOKEN) headers.Authorization = `Bearer ${GITHUB_TOKEN}`;
const res = await fetch(url, { headers });
if (!res.ok) {
const resetAt = Number(res.headers.get("x-ratelimit-reset") || 0) * 1000;
const resetSuffix = resetAt ? ` (resets ${new Date(resetAt).toISOString()})` : "";
if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
throw new Error(
`GitHub 403 rate limit exceeded${resetSuffix} for ${url}. Set GITHUB_TOKEN or GH_TOKEN to increase the limit.`,
);
}
throw new Error(`GitHub ${res.status} ${res.statusText} for ${url}`);
}
return res;
}
function resolveRepoHead(repo) {
const result = spawnSync(
"git",
["ls-remote", "--symref", `https://github.com/${repo}.git`, "HEAD"],
{
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
},
);
if (result.status !== 0) {View on GitHub (pinned to 0ec725320d)