Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/rate-limit-guard/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "rate-limit-guard",
"version": "0.3.4",
"version": "0.3.5",
"description": "Shared rate-limit guard for loop lanes: a statusline wrapper tees the subscription rate-limit windows to a fixed machine-scope file, a StopFailure hook records rate-limit stops reactively, and a reader contract fixes how consuming sessions pause and resume.",
"author": {
"name": "Melodic Software",
Expand Down
23 changes: 23 additions & 0 deletions plugins/rate-limit-guard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
All notable changes to the `rate-limit-guard` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.3.5]

### Fixed

- **The tee's `account` forward-pass no longer promises a no-change upgrade path it cannot deliver
(#1685).** Four surfaces claimed that the release adding an account identifier upgrades the tee
file for free β€” the reader contract's tee-shape bullet and single-account gap invariant, the
README's known-gap bullet ("the wrapper automatically adopts any future account-identifying field
the schema grows"), and the tee script's own header comment. Each described the writer accurately
and then drew a conclusion broader than it supports. The writer selects on the **top-level key
name only** (`to_entries` over the root object), matching `account` as a **case-insensitive
substring**; an unmatched key is dropped with no diagnostic, in a contract that fail-closes on
every other unresolvable input. The promise therefore holds only when the new field's own
top-level key name contains `account`: `user`, `identity`, `org`, `seat`, and an `account_uuid`
buried inside a non-matching object all vanish silently. All four surfaces now scope the claim to
that shape and say every other shape needs a writer change.
- **The reader contract now states that a forward-passed key carries its whole value.** A selected
top-level key crosses complete, nested objects included (`account_info: {uuid, display_name}`), so
the untrusted-value discipline is restated to cover an **object of arbitrary strings** rather than
only a scalar β€” the parse-with-a-JSON-parser, never-interpolate rule applies to the whole subtree.
- `statusline-tee.sh`'s **behavior is unchanged**; only its header comment was corrected. Widening
the filter is a design question owned by `TODO(#1218)`, not this correction.

## [0.3.4]

### Fixed
Expand Down
5 changes: 3 additions & 2 deletions plugins/rate-limit-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ resume on their own after the reset. Four parts:
- **Multi-account operation is a known gap, not a supported mode.** The snapshot carries no account
identifier (none exists in the statusline schema today), so a machine switching accounts mid-drain
feeds wrong windows to running lanes and the guard cannot detect it. The loop-lane convention Β§6
owns that framing; the reader contract cites it. The wrapper automatically adopts any future
account-identifying field the schema grows.
owns that framing; the reader contract cites it. The wrapper adopts a future account-identifying
field automatically only when its own top-level key name contains `account`; every other shape
needs a writer change (`reference/reader-contract.md`, "Tee file shape").

## Install

Expand Down
23 changes: 15 additions & 8 deletions plugins/rate-limit-guard/reference/reader-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,18 @@ never sees torn JSON; the file is **last-writer-wins** across all sessions on th
(<https://code.claude.com/docs/en/statusline>, verified 2026-07-23): `used_percentage` is 0–100,
`resets_at` is Unix epoch seconds. The key is present **only** when the session observes
subscription windows; each window may be independently absent.
- Session-distinguishing fields β€” `session_id`, `session_name`, and any future top-level field whose
key matches `account` (case-insensitive) are copied through automatically, so the release that
adds an account identifier upgrades this file without a plugin change. Treat these values as
**untrusted**: `session_name` (and potentially a future account field) is user/AI-influenced, so
consumers parse them only with a JSON parser and never string-interpolate them into a shell
command, another interpreter, or a prompt.
- Session-distinguishing fields β€” `session_id`, `session_name`, and any **top-level** key whose name
**contains** `account` (case-insensitive) are copied through automatically. The writer selects on
the **top-level key name only**, and a selected key carries its **whole value** across, nested
objects included: `account_info: {uuid, display_name}` arrives complete. A key that does not match
is dropped with no diagnostic β€” `user`, `identity`, `org`, and `seat` all vanish silently, and so
does an `account_uuid` buried inside a non-matching object such as `user`, because nothing at the
top level matched. A future account identifier therefore arrives without a writer change only when
its own top-level key name contains `account`; every other shape needs one. Treat these values as
**untrusted**: `session_name`, and any future account field β€” which may be an **object of
arbitrary strings**, not just a scalar β€” are user/AI-influenced, so consumers parse them only with
a JSON parser and never string-interpolate them into a shell command, another interpreter, or a
prompt.

## Capability detection (fail-open)

Expand Down Expand Up @@ -104,8 +110,9 @@ is part of the seam only in the sense that tooling sweeping the directory should
on the first, and the guard cannot detect it. The loop-lane convention Β§6 owns the framing and
records it as a gap rather than as a safe assumption; the account-identity design that resolves
it β€” writer-side field, reader-side invalidation of latched state, lane-floor re-audit β€” is
`TODO(#1218)`. Locally relevant today: the writer already forward-passes any top-level key
matching `account`, so an identity field costs no plugin change the release one appears.
`TODO(#1218)`. Locally relevant today, and only this far: the writer already forward-passes an
account-matching key under the exact rule "Tee file shape" states, so an identity field of that
shape costs no writer change the release one appears β€” and every other shape costs one.
- **No shipped Monitor config.** Consumers arm their own session Monitor on the tee file (the
staleness rule makes this mandatory while paused). The plugin ships no `experimental.monitors`
entry β€” Monitors is an experimental Claude Code component, and this plugin takes no dependency on
Expand Down
6 changes: 4 additions & 2 deletions plugins/rate-limit-guard/scripts/statusline-tee.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
# side): every refresh writes ~/.claude/rate-limit-guard/rate-limits.json β€”
# one JSON object with captured_at (ISO-8601 UTC) plus, when present on
# stdin, rate_limits and every session-distinguishing top-level field
# (session_id, session_name, and any key matching "account", so a future
# account-identifier field is adopted automatically the release it appears).
# (session_id, session_name, and any key whose name contains "account", so a
# future account-identifier field is adopted automatically only when it
# arrives under a top-level key of that shape; every other shape needs a
# writer change).
# The path is deliberately HOME-anchored and outside ${CLAUDE_PLUGIN_DATA}:
# it is a documented cross-plugin artifact seam that sibling-plugin lane
# sessions read, machine-scope by design (the file is last-writer-wins and
Expand Down