Predicting when you'll hit your Claude usage limit, without scraping the page A developer built Claude Usage Companion, a free, open-source browser extension that predicts when a user will hit Claude's 5-hour session and 7-day weekly usage limits. The extension captures undocumented JSON from claude.ai's /api/organizations/{org_id}/usage endpoint, message_limit events in the chat SSE stream, and rate_limit_event entries in Claude Code session logs, rather than scraping page HTML, and runs entirely locally with no data leaving the machine. A few weeks ago I was in the middle of a refactor with Claude when the reply just stopped: "You've reached your session limit. Resets at 5:00 PM." I had no idea I was close. That's what bugged me. Claude's paid plans have two limits, a 5-hour session window and a 7-day weekly one, and claude.ai and Claude Code draw from the same pool. The numbers are sitting right there under Settings → Usage. But nothing ever says "at this rate you're done in 40 minutes." You find out when you hit it. So I built a browser extension that tells you. It's called Claude Usage Companion. It's free, it's open source, and nothing leaves your machine. This post is about the parts that turned out more interesting than I expected, most of which come down to building on an API that isn't documented and doesn't sit still. A percentage on its own isn't that useful. 62% an hour into the window and 62% four and a half hours in are completely different situations. One means slow down, the other means you're fine. What I wanted was a single line: At this pace, session runs out ~3:50 PM, before it resets. If I see that, I can do something about it. Switch to a lighter model, finish the risky part of the task first, or go get lunch. Most of the extension exists to make that one line trustworthy. There's no public API for this, so I opened DevTools on claude.ai and just used it for a while. Later I had Claude in Chrome drive a real session and capture the responses, so I'd have clean fixtures to build schemas from. Three sources turned up. The first is GET /api/organizations/{org id}/usage . It's what the Settings → Usage page calls: a five hour and a seven day window, each with a utilization percent and a reset time, with plain cookie auth. The catch is that claude.ai's frontend only calls it from that settings page. If you never open it, your browser never sees the data. The second is hiding in the chat stream. When you send a message, the reply streams back as server-sent events, and near the end, between message delta and message stop , there's a message limit event with the same two windows as fractions: { "type": "within limit", "representativeClaim": "five hour", "windows": { "5h": { "status": "within limit", "resets at": 1788007200, "utilization": 0.29 }, "7d": { "status": "within limit", "resets at": 1788008400, "utilization": 0.55 } } } The third is on claude.ai/code, where Claude Code sessions show up on the web. The session event log has rate limit event entries mixed in with everything else. So there's a snapshot I can request whenever I want, and live updates that arrive with every reply while you're actually working. I use both. A lot of usage trackers read numbers out of the page's HTML. That works until the frontend gets redesigned, which happens often. The JSON behind the page changes less, so I capture that instead. The awkward part is Chrome's content script model. By default a content script runs in an "isolated world": it can see the DOM but not the page's JavaScript, so it can't see the page's fetch calls. To wrap fetch , a script has to run in the page's main world, and main-world scripts can't talk to the extension. So there are two scripts: one in the main world that captures, and one in the isolated world that just passes messages along. claude.ai tab ┌──────────────────────────────────────────────┐ │ MAIN world: hook │ wraps window.fetch, │ /usage, completion SSE, code events │ reads a clone │ │ window.postMessage │ │ ISOLATED world: relay ───────────────────────┼── runtime message └──────────────────────────────────────────────┘ │ ▼ background service worker validate → normalize → IndexedDB forecast, alerts, badge The hook is short. The one rule I cared about is that it can never break claude.ai: js const originalFetch = window.fetch.bind window ; window.fetch = async ...args = { const response = await originalFetch ...args ; try { // Read a clone. The page gets the original, untouched. void handleResponse response.clone , getRequestUrl args 0 ; } catch { // A bug here can lose a data point. // It must never affect the page. } return response; }; The chat stream isn't an EventSource . claude.ai calls fetch and reads the body as a ReadableStream , so if you go looking for EventSource you'll find nothing. You have to read the cloned body yourself and split it into SSE frames, keeping a buffer because a frame can arrive split across two chunks: js const reader = response.body.getReader ; const sse = new SseFrameBuffer ; for ;; { const { done, value } = await reader.read ; if value { for const frame of sse.push value handleFrame frame ; } if done { for const frame of sse.flush handleFrame frame ; return; } } // handleFrame ignores everything except 'message limit' The Claude Code event log needed more care, because most of what's in it is your actual conversation: user and assistant messages. The hook throws away everything except rate limit event before anything leaves the page, so conversation text never reaches the extension's storage at all. Then there's the settings-page problem. Once the extension has seen your org ID in a usage or chat request, the background worker calls /usage itself every 5 minutes, using the session cookie you already have. That way the bars stay fresh even if you never open Settings again. I captured the same account's /usage response twice, three days apart. In between, a locked reason field appeared on each window that hadn't been there before, not even as null . A new top-level key showed up too, always null so far. And some sub-objects, like the spend limit, flipped between a full object and null depending on whether usage credits were turned on. If I'd written a strict schema from the first capture, it would have accepted the second one fine. If I'd written it from the second, it would have rejected the first. So I settled on three rules: zod 's safeParse . If a payload doesn't match, it gets logged and skipped. Nothing throws. This protects against frontend redesigns, not against the API itself changing. If the response format changes enough, the extension stops updating until I ship a fix. I'd rather it go quiet than show wrong numbers. It's least squares through recent snapshots. The only real decision is which snapshots to use. A window resets: the percentage drops back toward zero. Fit a line across that drop and the result is garbage. So I only use the current run. Walk backward from the newest point and stop at the first place the percentage went down: js // history sorted oldest → newest let runStart = sorted.length - 1; for let i = sorted.length - 1; i 0; i-- { // a drop means the window reset if sorted i .percent < sorted i - 1 .percent break; runStart = i - 1; } const run = sorted.slice runStart ; if run.length < 2 return null; // can't fit a line to one point After that it's ordinary least squares on hours-since-run-start against percent, solved for where the line reaches 100. Confidence is just sample size: fewer than 3 points is low, 3 or more is medium, and 5 or more spread over at least an hour is high. It's not a real confidence interval, and the code comments say so. I didn't try anything smarter because usage is bursty. You prompt hard for 20 minutes, then read code for 40. Nothing I could build would predict when you'll open the next chat. A straight line answers the question you actually have "at the pace I've been going, do I make it?" , and when it's wrong you can see why. One edge case: a tiny but positive rate, where usage is barely moving, projects the "full" time thousands of years out, past what a JavaScript Date can hold. new Date doesn't throw on that; it quietly gives you an Invalid Date, and then toISOString does throw. That would crash the dashboard, so there's a check for it now. This turned out to matter more than the math. A warning that fires all the time gets ignored. The popup only shows a headline when something needs attention: a limit you've already hit, or a forecast that says you'll run out early. For the forecast to count, three things have to be true. It has at least medium confidence. At least 10% of the window is used, because 2% in the first five minutes technically projects to "full in four hours," which is useless. And the projected run-out lands at least 30 minutes before the reset; if it only beats the reset by a few minutes, it's not worth interrupting you. Otherwise the popup just shows the bars. There's also an optional notification for the same forecast, off by default in Settings. It fires once per limit window. Deduplicating that was fiddlier than I expected: /usage and the chat stream report the same reset time with slightly different precision, so the dedupe key rounds the reset time to the hour. Otherwise the same warning would fire twice. Separately, plain threshold notifications at 80% and 95% are on by default. A browser can't see your terminal. But Claude Code keeps session transcripts in ~/.claude/projects/ / .jsonl , with token usage and the model on each response and the working directory on each line. So there's an optional local daemon, a small Node + Hono server, that reads them. It uses ccusage https://github.com/ccusage/ccusage in --offline mode, so it never fetches pricing data for token totals by project and model, and reads the transcripts directly for full-text search across all your sessions, with a copy-paste claude --resume