[BUG] ghRateLimitHint prescribes `gh api rate_limit` as the diagnostic, but that endpoint can report a different bucket than the one enforced (reads full during a block)
Preflight
- [x] Searched existing issues (
ghRateLimitHint, "rate limit exceeded system-reminder", "gh api rate_limit") — the closest matches, #51988 and #52745, are about when the hint fires. This is about what it tells the model to do, which neither covers. Both are closed and locked; their auto-lock message invites a new issue referencing them. - [x] Single bug report.
- [x] Latest version (2.1.220).
What's Wrong?
The ghRateLimitHint <system-reminder> names one diagnostic and one remedy:
GitHub API rate limit exceeded (5,000/hr shared across all tools and agents). Run `gh api rate_limit --jq .resources` and sleep until reset before further gh calls. If polling in a loop, use ScheduleWakeup instead of retrying.
gh api rate_limit can report different counters from the ones actually enforced on the same token's REST calls, so an agent that follows the hint literally sees a gauge reading nearly full during a genuine block, concludes it is not rate-limited, and retries into the same 403 — or, if it does sleep, sleeps to an epoch ~26 minutes later than the real one.
Verified on one machine, one gh token (GH_TOKEN/GITHUB_TOKEN unset, single hosts.yml credential), one shell, calls seconds apart. Both responses label themselves Resource: core:
$ gh api rate_limit --jq '.resources.core'
{"limit":5000,"remaining":4997,"reset":1785245820,"used":3}
$ gh api repos/anthropics/claude-code -i | grep -i '^x-ratelimit'
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4658
X-Ratelimit-Reset: 1785244227
X-Ratelimit-Resource: core
X-Ratelimit-Used: 342
used: 3 vs used: 342, and the two reset epochs are 1593 seconds apart. Repeating the pair minutes apart, the gauge stayed frozen at used: 3 while the enforced counter climbed 283 → 316 → 342. Extrapolate the enforced counter to 5000 and you get the reported symptom exactly: every REST call 403s while gh api rate_limit still reads nearly full.
I can't tell you why the two diverge — that's your/GitHub's side of the fence — and the fix doesn't need it. The point is only that the endpoint the hint names is not a reliable read of the pool being enforced.
Downstream cost we actually paid, in an unattended automation loop: one agent made 11 retry attempts over ~15 minutes because rate_limit kept reporting ~4,993 core remaining; a second agent independently re-derived the same dead end; and a third followed the hint into gh auth status, which decides token validity by making an API call and therefore reported "The token ... is invalid. To re-authenticate, run: gh auth login" under the block — costing a human an interactive device flow for a credential that was never bad (a genuinely bad credential returns 401, never 403).
What Should Happen?
The hint should name the diagnostic that is per-request and reflects the enforced pool: the failing response's own headers.
Suggested replacement text:
GitHub API rate limit exceeded. Re-run the failing call with-i(gh api <path> -i) and readX-Ratelimit-Resource/X-Ratelimit-Remaining/X-Ratelimit-Reset; sleep until thatResetepoch before further gh calls.gh api rate_limitmay report a different bucket and can read full during a block. If polling in a loop, use ScheduleWakeup instead of retrying.
Two properties that make this strictly better and no more complex:
- It requires no primary-vs-secondary-limit classification. (Our own first hypothesis — "a 403 while
rate_limit.core.remainingis high must be the secondary limit" — turned out to be wrong: response headers captured during a real block readX-Ratelimit-Resource: core,Used: 5000, i.e. the primary limit. "GraphQL still works" is not a discriminator either, since GraphQL has its own separate 5,000 pool that survives any core exhaustion.) - The header's
Resetis empirically the right epoch to wait on: during a real block we slept toX-Ratelimit-Resetand REST recovered exactly at it, first probe.
Error Messages/Logs
$ gh api repos/<owner>/<repo>/pulls/<n> --jq '.title'
{ "message": "API rate limit exceeded for user ID <redacted>. ...", "status": "403" }
$ gh api rate_limit --jq '{core: .resources.core}'
{"core":{"limit":5000,"remaining":4993,"reset":<epoch>,"used":7}}
Steps to Reproduce
The divergence (no rate limiting required — it is visible on an idle token):
gh api rate_limit --jq '.resources.core'— noteused/reset.gh api repos/anthropics/claude-code -i | grep -i '^x-ratelimit'— noteX-Ratelimit-Used/-Reset.- Repeat step 1. The gauge does not track the enforced counter, and the two
resetepochs differ (1593s apart here). - Sustain enough REST traffic on that token to exhaust the enforced counter; calls 403 while step 1 still reads nearly full — which is the state the hint's prescribed diagnostic is supposed to detect and doesn't.
Related, previously reported: the hint still fires on successful calls (2.1.220)
Not the bug above, but it compounds it — a false fire sends the model to the same unreliable gauge. #51988 and #52745 both reported this and are closed; it reproduces today on 2.1.220 with a single command:
$ gh issue view 51988 -R anthropics/claude-code --json body --jq '.body' | grep -m1 "API rate limit exceeded"
<system-reminder>GitHub API rate limit exceeded (5,000/hr shared across all tools and agents). ...</system-reminder>
EXIT=0
<system-reminder>GitHub API rate limit exceeded (5,000/hr shared across all tools and agents). Run `gh api rate_limit --jq .resources` and sleep until reset before further gh calls. If polling in a loop, use ScheduleWakeup instead of retrying.</system-reminder>
The indented line is the quoted text inside issue #51988's body; the flush-left line is the hint appended to the tool result. The gh call returned HTTP 200 and the enforced counter stood at 342/5000 at the time — nothing was rate-limited. The 2.1.117 half of #51988 does appear fixed: a bare echo "API rate limit exceeded for user" (no gh in the command) no longer fires it, and neither did gh search issues --repo anthropics/claude-code --match title "rate limit". What still fires it is a gh command whose stdout contains the phrase.
Claude Model
Opus
Is this a regression?
No, this never worked
Claude Code Version
2.1.220 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Non-interactive/CI environment
Additional Information
Reported from an unattended maintenance loop that runs gh continuously; the misdirection is expensive there because no human is watching to notice the gauge is lying. We are shipping a repo-level doctrine note prescribing the header read as a local mitigation, but the hint reaches every agent in every repo, so the fix belongs here.