{"record":{"id":"5f5da7a704ef5f13","repo":"midudev/autoskills","slug":"github-403-rate-limit-exceeded-resetsuffix-for-url-set","errorCode":null,"errorMessage":"GitHub 403 rate limit exceeded${resetSuffix} for ${url}. Set GITHUB_TOKEN or GH_TOKEN to increase the limit.","messagePattern":"GitHub 403 rate limit exceeded(.+?) for (.+?)\\. Set GITHUB_TOKEN or GH_TOKEN to increase the limit\\.","errorType":"http","errorClass":"Error","httpStatus":403,"severity":"error","filePath":"packages/autoskills/scripts/sync-skills.mjs","lineNumber":157,"sourceCode":"  }\n  return byRepo;\n}\n\n// ── GitHub helpers ───────────────────────────────────────────\n\nasync function ghFetch(url) {\n  const headers = {\n    \"User-Agent\": \"autoskills-sync\",\n    Accept: \"application/vnd.github+json\",\n    \"X-GitHub-Api-Version\": \"2022-11-28\",\n  };\n  if (GITHUB_TOKEN) headers.Authorization = `Bearer ${GITHUB_TOKEN}`;\n  const res = await fetch(url, { headers });\n  if (!res.ok) {\n    const resetAt = Number(res.headers.get(\"x-ratelimit-reset\") || 0) * 1000;\n    const resetSuffix = resetAt ? ` (resets ${new Date(resetAt).toISOString()})` : \"\";\n    if (res.status === 403 && res.headers.get(\"x-ratelimit-remaining\") === \"0\") {\n      throw new Error(\n        `GitHub 403 rate limit exceeded${resetSuffix} for ${url}. Set GITHUB_TOKEN or GH_TOKEN to increase the limit.`,\n      );\n    }\n    throw new Error(`GitHub ${res.status} ${res.statusText} for ${url}`);\n  }\n  return res;\n}\n\nfunction resolveRepoHead(repo) {\n  const result = spawnSync(\n    \"git\",\n    [\"ls-remote\", \"--symref\", `https://github.com/${repo}.git`, \"HEAD\"],\n    {\n      encoding: \"utf-8\",\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n    },\n  );\n  if (result.status !== 0) {","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/midudev/autoskills/blob/0ec725320d2137253ab2e68e7ba8a072148e741a/packages/autoskills/scripts/sync-skills.mjs#L139-L175","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$ node scripts/sync-skills.mjs   // 403, rate limit exhausted\n\n// after\n$ GITHUB_TOKEN=ghp_xxx node scripts/sync-skills.mjs","handlingStrategy":"retry","validationCode":"const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;\nif (!token) throw new Error(\"Set GITHUB_TOKEN before running the sync script\");\n// optionally pre-check budget\nconst rl = await fetch(\"https://api.github.com/rate_limit\", { headers: { Authorization: `Bearer ${token}` } }).then(r => r.json());\nif (rl.resources.core.remaining < 20) throw new Error(`Low GitHub quota: ${rl.resources.core.remaining}`);","typeGuard":null,"tryCatchPattern":"import { setTimeout as sleep } from \"node:timers/promises\";\nasync function ghFetchWithRetry(url, tries = 2) {\n  try {\n    return await ghFetch(url);\n  } catch (e) {\n    const m = e.message.match(/resets (.+?)\\)/);\n    if (m && tries > 0) {\n      await sleep(Math.max(0, new Date(m[1]).getTime() - Date.now()) + 1000);\n      return ghFetchWithRetry(url, tries - 1);\n    } else throw e;\n  }\n}","preventionTips":["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."],"tags":["github","rate-limit","api","authentication"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"0ec725320d2137253ab2e68e7ba8a072148e741a","analyzedAt":"2026-09-15T14:12:31.090Z","contentChangedAt":"2026-09-15T14:12:31.090Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}