Writes the account's rate-limit windows (five_hour, seven_day) to ~/.claude/local/rate-limits.json on every measurement, for the weekly readout fleet-launch.sh…

A hooks-only mod that writes the account's rate-limit windows to ~/.claude/local/rate-limits.json on every measurement, so scripts/fleet-launch.sh can print the active account's burst (five_hour) and weekly (seven_day) readings before it dispatches, and scripts/fleet-headroom.sh can park a fleet session on the same meter. Nothing at launch reported weekly use before it: the 2026-10-10 fleet (3 sessions, 12h) was launched on an account 5.75h from its weekly limit and lost 18.75 of its 36 session-hours.
session.measure fires after each main-thread turn and whenever a window moves a whole point. When its rateLimits carries a reading the whole file is rewritten — last writer wins, so the file names whichever account the latest session on this machine ran on, and measured_at dates it. A measurement with no windows (off a subscription, or before the first response) writes nothing, so an empty reading never overwrites a dated one.
{
"measured_at": "2026-10-10T20:15:03.000Z",
"session": "8c16860d-d02b-4364-af52-2d5a27d9dd9c",
"windows": [
{ "kind": "five_hour", "percentUsed": 12.5, "resetsAt": "2026-10-11T00:00:00.000Z" },
{ "kind": "seven_day", "percentUsed": 71, "resetsAt": "2026-10-13T22:00:00.000Z" }
]
}
~/.claude/local/ is the directory ~/.claude/CLAUDE.md reserves for machine-local state kept across sessions; the write creates it. The home directory comes from HOME, or USERPROFILE where only that is set.
Each session also keeps a history, ~/.claude/local/rate-limits/<session-id>.jsonl, one line per measurement in which a window moved ({"measured_at", "windows"}, the same windows as above). The snapshot cannot say how full the 5-hour window got while a fleet ran — the 2026-10-12 retro had only a reading from two days earlier to estimate headroom from — and one file per session means concurrent sessions never race a shared file. scripts/fleet-metrics.py reads a fleet's files for each session's peak five_hour reading, and scripts/fleet-headroom.sh --session <id> reads a session's last line as its account's current 5h reading at pick time (its own account by construction), falling back to the snapshot. A reload resumes the session's file rather than starting it over. Nothing prunes these files (the mod API has no delete); each holds at most a few hundred short lines.
A headless claude -p --plugin-dir ~/.claude/mods/rate-limits --model haiku run on a subscription account wrote the file on its one turn, carrying both windows: five_hour at 28% resetting in 3.5h and seven_day at 7% resetting Oct 13 22:00Z, the same reset the morning's weekly-limit message had named. The file did not exist before the run. Not yet measured: that the reading follows an account switch — the next session on the other account rewrites the file, and measured_at should move with it.
userConfig: enabled (kill switch).
Installed user-wide by update.sh from the repo's marketplace and loaded in place from this folder. fleet-launch.sh never requires it: without a reading it prints Weekly: no reading (<why>) and launches.
claude plugin validate mods/rate-limits && claude plugin test mods/rate-limits && mods/typecheck.sh mods/rate-limitshooks/register.ts 53 lines1import type { Register } from 'claude-code'
2import { historyLine, historyPath, readOptions, readingOf, readingPath } from './limits'
3
4let sessionId = ''
5let path: string | null = null
6let historyFile: string | null = null
7let history: string[] = []
8
9export const register: Register = (on, options) => {
10 const opts = readOptions(options)
11
12 on('session.start', async ($, e, next) => {
13 sessionId = await $.session.id()
14 const home = (await $.env.get('HOME')) ?? (await $.env.get('USERPROFILE'))
15 path = readingPath(home)
16 historyFile = historyPath(home, sessionId)
17 // A reload starts the module over; the session's file is where its earlier readings live.
18 history = []
19 if (historyFile !== null && (await $.fs.exists(historyFile))) {
20 history = (await $.fs.read(historyFile)).split('\n').filter((line) => line !== '')
21 }
22 return next(e)
23 })
24
25 // Fires after each main-thread turn and whenever a window moves a whole point. The whole file is rewritten each time, last writer
26 // wins, so it names whichever account the latest session on this machine ran on and `measured_at` dates it. The session's history
27 // gains a line only when a window moved. A failed write is dropped: a readout never touches the session, and the launch line says
28 // when the file is stale or missing.
29 on('session.measure', async ($, e, next) => {
30 if (opts.enabled && path !== null) {
31 const now = await $.clock.now()
32 const text = readingOf(e.rateLimits, sessionId, now)
33 if (text !== null) {
34 try {
35 await $.fs.write(path, text)
36 } catch {
37 // see above
38 }
39 }
40 const line = e.changed.includes('rateLimits') ? historyLine(e.rateLimits, now) : null
41 if (line !== null && historyFile !== null) {
42 history.push(line)
43 try {
44 await $.fs.write(historyFile, `${history.join('\n')}\n`)
45 } catch {
46 // see above
47 }
48 }
49 }
50 return next(e)
51 })
52}
53hooks/limits.ts 38 lines1export type RateLimit = { readonly kind: string; readonly percentUsed: number; readonly resetsAt?: string }
2export type Options = { readonly enabled: boolean }
3
4export function readOptions(raw: Readonly<Record<string, unknown>>): Options {
5 return { enabled: raw.enabled !== false }
6}
7
8// One machine-wide file under ~/.claude/local/, the directory ~/.claude/CLAUDE.md reserves for state kept across sessions, read by
9// scripts/fleet-launch.sh. Null without a home directory, and then nothing is written.
10export function readingPath(home: string | undefined): string | null {
11 const h = (home ?? '').trim().replace(/[\\/]+$/, '')
12 return h === '' ? null : `${h}/.claude/local/rate-limits.json`
13}
14
15function windowsOf(windows: readonly RateLimit[]) {
16 return windows.map((w) => (w.resetsAt === undefined ? { kind: w.kind, percentUsed: w.percentUsed } : { kind: w.kind, percentUsed: w.percentUsed, resetsAt: w.resetsAt }))
17}
18
19// The file's text: the windows as the engine reported them, dated; null when the measurement carried none (off a subscription, or
20// before the first response), so an empty reading never overwrites a dated one.
21export function readingOf(windows: readonly RateLimit[], session: string, nowMs: number): string | null {
22 if (windows.length === 0) return null
23 return `${JSON.stringify({ measured_at: new Date(nowMs).toISOString(), session, windows: windowsOf(windows) }, null, 2)}\n`
24}
25
26// The snapshot above keeps only the latest writer, so it cannot say how full a window got while a fleet ran. Each session also keeps
27// its own history, one file per session so concurrent sessions never race a shared one; fleet-metrics.py reads a fleet's files for
28// the peak five_hour reading. Null without a home directory or a session id.
29export function historyPath(home: string | undefined, session: string): string | null {
30 const h = (home ?? '').trim().replace(/[\\/]+$/, '')
31 return h === '' || session === '' ? null : `${h}/.claude/local/rate-limits/${session}.jsonl`
32}
33
34export function historyLine(windows: readonly RateLimit[], nowMs: number): string | null {
35 if (windows.length === 0) return null
36 return JSON.stringify({ measured_at: new Date(nowMs).toISOString(), windows: windowsOf(windows) })
37}
38