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

  1. Set the GITHUB_TOKEN (or GH_TOKEN) environment variable with a valid personal access token to raise the rate limit.
  2. Wait until the reset time reported in the error message, then retry the install.
  3. If a token is already set, verify it is valid and not expired/revoked.
  4. 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

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


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)