StopFailure: error "rate_limit" covers both a resumable usage-limit freeze and a non-resumable credits condition, and the reset time is only in prose
Summary
I journalled every StopFailure hook payload on one machine for ~3 weeks to characterise what an account usage-limit freeze looks like to a hook. Two things make it hard to act on programmatically:
error: "rate_limit"is not a sufficient discriminator. It covers both a usage window that will expire, and a condition that will not (a model requiring credits). A tool that treatsrate_limitas "wait and retry" will retry forever against the second case.- The reset time exists only as English prose in
last_assistant_message. There is no structured field, noretry_after, no timestamp.
The data
43 StopFailure payloads, 2026-08-18 → 2026-09-07, one machine:
| error | count |
|---|---:|
| server_error | 26 |
| rate_limit | 11 |
| authentication_failed | 4 |
| invalid_request | 2 |
Of the 11 rate_limit samples, 9 are genuine usage-limit freezes and 2 are not.
Genuine freeze — reset time appears only here, in last_assistant_message:
… resets 4pm (America/Los_Angeles) × 8
… resets 12am (America/Los_Angeles) × 1
Properties: local wall-clock, human-formatted, explicit IANA zone in parentheses, no date. error_details is absent on all 9.
Not a freeze — same error: "rate_limit", but error_details present and describing a non-expiring condition:
{
"error": "rate_limit",
"error_details": "429 {\"type\":\"error\",\"error\":{\"type\":\"rate_limit_error\",
\"message\":\"Usage credits are required for this model.\",
\"details\":{\"error_code\":\"credits_required\",\"model\":\"<model>\",
\"disabled_reason\":\"org_level_disabled\",\"exhausted_included_allowance\":false}}}"
}
No reset time, because there is nothing to wait for.
Bursts are normal: six of the eight 4pm samples arrived within 90 seconds of each other — one freeze producing many StopFailure events. Anything scheduling work off this needs to dedupe.
What would help
Any one of these would be enough, in rough order of usefulness:
- A structured reset timestamp in the payload when one exists — ISO-8601 or epoch. Parsing
resets 4pm (America/Los_Angeles)with no date means resolving "next future occurrence of that wall-clock time in that zone", which is a lot of inference for something the server already knows exactly. - A discriminator between expiring and non-expiring conditions — either a distinct
errorvalue, or a documented guarantee that a reset-bearing freeze always/never carrieserror_details. - Failing both, documentation of the payload shape per
errorvalue, so hook authors are not reverse-engineering it from captured samples.
Context
Observed fields across these payloads: session_id, transcript_path, cwd, scratchpad_dir, prompt_id, effort, hook_event_name, error, error_details (2/11), last_assistant_message.
The use case is a watchdog that resumes a session after its limit resets — which necessarily runs outside the frozen session, since the frozen session has no tokens left to notice anything. That makes the hook payload the only available signal, and the reset time the only thing that determines when to act.
Environment
macOS 25.6.0 (arm64). Samples span several Claude Code versions through 2.1.282.