[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)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments · opened Jul 28, 2026

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 read X-Ratelimit-Resource / X-Ratelimit-Remaining / X-Ratelimit-Reset; sleep until that Reset epoch before further gh calls. gh api rate_limit may 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:

  1. It requires no primary-vs-secondary-limit classification. (Our own first hypothesis — "a 403 while rate_limit.core.remaining is high must be the secondary limit" — turned out to be wrong: response headers captured during a real block read X-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.)
  2. The header's Reset is empirically the right epoch to wait on: during a real block we slept to X-Ratelimit-Reset and 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):

  1. gh api rate_limit --jq '.resources.core' — note used / reset.
  2. gh api repos/anthropics/claude-code -i | grep -i '^x-ratelimit' — note X-Ratelimit-Used / -Reset.
  3. Repeat step 1. The gauge does not track the enforced counter, and the two reset epochs differ (1593s apart here).
  4. 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.

View original on GitHub ↗