When the API ends the turn instead of Claude
The turn should have finished with Claude choosing to stop. Instead the API killed it: a rate limit, an overloaded server, a failed login. Claude did not choose to end it. Stop does not fire. StopFailure fires in its place, to record the failure.
Ten error types to match
You probably do not want one hook firing on every API failure. The matcher filters the error type: rate_limit, overloaded, authentication_failed, oauth_org_not_allowed, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, or unknown. A hook that pages someone about billing matches billing_error alone and never wakes for a rate limit.
The payload carries error, an optional error_details, and an optional last_assistant_message. That last field is not something Claude said. It holds the rendered API error text, because the turn ended before Claude produced a reply.
Nothing you return is read, except one field
Do not wire this hook expecting to retry the turn or show a message on screen. Exit 0, exit 1, exit 2: the failed turn stays failed. The hooks reference says Claude Code ignores both the output and the exit status of a StopFailure hook, with one exception. An allowed terminalSequence still fires. So the hook is for logging the failure, sending an alert, or starting a recovery somewhere else. Do not write it expecting to change what the user sees, because nothing it returns can.
You return {"systemMessage": "Rate limited. Retrying in 60 seconds."} and exit 0, expecting the line on screen.
Nothing appears. The field is ignored along with every other field on this event, and the turn is over. The one output that reaches the terminal is terminalSequence, whose allowed values the reference lists under its terminal notification section.
For anything more than a terminal effect, write to a log file or call your alerting tool from inside the hook.