1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
version: 1
vendor: claude
# `claude --output-format stream-json` emits one JSON record per line, and the
# stream carries both directions: the vendor's own messages *and* every file
# the agent read back through a tool. Without framing the classifier cannot
# tell them apart, so an agent reading a fixture that quotes a rejection reads
# as the vendor issuing one.
framing: ndjson
rules:
- id: claude.rate-limit.usage-limit
class: rate_limited
diagnostic: Claude usage limit reached
match:
# No stream constraint: the vendor's refusal arrives on stdout as a
# synthetic assistant message, so a stderr-only rule is unreachable here.
#
# Only the vendor authors these two records. `is_api_error_message` marks
# the synthetic message; the result record's `is_error` is the vendor's
# own top-level verdict on the run. Predicates read top-level fields, so
# a rejection quoted inside a tool result — an escaped string in some
# other record's field — never satisfies either. Lines that are not JSON
# stay eligible, which keeps the buffered `--print` form classifying.
record_any_of:
- is_api_error_message: true
- type: result
is_error: true
# Signatures are conjunctive, so the one signature has to be the wording
# every limit shares. Claude qualifies the noun per limit window
# ("your limit", "your session limit", "your weekly limit"); the phrase
# up to that qualifier is what stays constant.
message_signatures:
- You've hit your
- id: claude.rate-limit.api-overloaded
class: rate_limited
diagnostic: Claude API overloaded
match:
# Observed as `API Error: 529 Overloaded. This is a server-side issue,
# usually temporary — try again in a moment.` A transient server-side
# overload, not a usage limit, but it earns the same conservative
# treatment: back off and retry later.
record_any_of:
- is_api_error_message: true
- type: result
is_error: true
message_signatures:
- 529 Overloaded