midudev/autoskills · error · Error
GitHub rate limit exceeded
Error message
GitHub rate limit exceeded${resetSuffix}. Set GITHUB_TOKEN or GH_TOKEN to increase the limit. What it means
When fetching a skill file from GitHub raw, a 403 response with the x-ratelimit-remaining header equal to 0 means GitHub's API rate limit is exhausted for the current (unauthenticated or token-bound) identity. The installer surfaces this explicitly, including the reset timestamp, because retrying immediately cannot succeed and the fix is authentication or waiting.
Solutions
- Set the GITHUB_TOKEN (or GH_TOKEN) environment variable with a valid personal access token to raise the rate limit.
- Wait until the reset time reported in the error message, then retry the install.
- If a token is already set, verify it is valid and not expired/revoked.
- Reduce request volume (install fewer skills at once) or use a custom registryBaseUrl that is not GitHub rate limited.
Example fix
// before (CI)
- run: pnpm autoskills install skill-a skill-b ...
// after
- run: pnpm autoskills install skill-a skill-b ...
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} Defensive patterns
Strategy: retry
Validate before calling
// check budget before a bulk install
const res = await fetch("https://api.github.com/rate_limit", {
headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}`.trim() || undefined },
});
const { remaining } = (await res.json()).resources.core;
if (remaining < estimatedRequests) throw new Error(`GitHub rate budget too low: ${remaining} left`); Try / catch
try {
await downloadRegistryEntry(name, entry, dest);
} catch (e) {
const m = e.message.match(/GitHub rate limit exceeded \(resets (.+?)\)/);
if (m) {
const waitMs = Math.max(0, new Date(m[1]).getTime() - Date.now()) + 1000;
await sleep(waitMs);
return downloadRegistryEntry(name, entry, dest); // retry once after reset
} else throw e;
} Prevention
- Always set GITHUB_TOKEN/GH_TOKEN in CI and local scripts that touch GitHub.
- For bulk operations, check GET /rate_limit first and throttle accordingly.
- Rotate tokens before expiry so a stale token never silently downgrades you to anonymous limits.
- Batch installs and avoid re-running full syncs unnecessarily.
When it happens
Trigger: downloadRegistryFile iterates its candidate base URLs and the fetch returns 403 + x-ratelimit-remaining: 0 — typically during bulk skill installs from an unauthenticated IP, or with a GITHUB_TOKEN whose rate limit is spent.
Common situations: CI runners on shared IPs installing many skills without a token; hitting the 60 req/hour unauthenticated limit on raw.githubusercontent/api endpoints; a leaked/revoked token causing unauthenticated classification.
Related errors
- GitHub 403 rate limit exceeded
- ⚠️ No se pudo crear la release en GitHub (¿tienes gh…
- GitHub for
- Tarball fetch failed
- raw fetch failed
AI-assisted analysis of midudev/autoskills@0ec725320d (2026-09-15).
Data as JSON: /api/errors/869829824a985ef1.
Report an issue: GitHub.
Appendix: source
Thrown at packages/autoskills/installer.ts:301
const expected = entry.sha256[rel] || entry.sha256[normalizedRel];
if (!expected) {
throw new Error(`no recorded hash for ${normalizedRel}`);
}
const fetchFile = opts.fetchImpl || fetch;
const errors = [];
for (const baseUrl of getRegistryRawBaseUrls(opts)) {
const url = `${baseUrl}/${encodeRawPath(skillName, normalizedRel)}`;
opts.onTrace?.(`GET ${url}`);
const res = await fetchFile(url, {
headers: githubDownloadHeaders(url),
});
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 rate limit exceeded${resetSuffix}. Set GITHUB_TOKEN or GH_TOKEN to increase the limit.`,
);
}
errors.push(`${res.status} ${res.statusText} from ${baseUrl}`);
opts.onTrace?.(`miss ${normalizedRel}: ${res.status} ${res.statusText} from ${baseUrl}`);
continue;
}
const buf = Buffer.from(await res.arrayBuffer());
const actual = sha256Buffer(buf);
if (actual !== expected) {
errors.push(`hash mismatch from ${baseUrl}`);
opts.onTrace?.(`hash mismatch for ${normalizedRel} from ${baseUrl}`);
continue;
}
opts.onTrace?.(`downloaded ${normalizedRel} from ${url}`);
return { buf, url };
}View on GitHub (pinned to 0ec725320d)