<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rehber Odhano</title>
    <description>The latest articles on DEV Community by Rehber Odhano (@rehberodhano).</description>
    <link>https://dev.to/rehberodhano</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4146020%2Ff3a6b3ab-9c0a-4456-92a4-83d8d953312c.jpg</url>
      <title>DEV Community: Rehber Odhano</title>
      <link>https://dev.to/rehberodhano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rehberodhano"/>
    <language>en</language>
    <item>
      <title>Claude usage jumped? How I added project, session and subagent attribution to my usage tracker</title>
      <dc:creator>Rehber Odhano</dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:28:52 +0000</pubDate>
      <link>https://dev.to/rehberodhano/claude-usage-jumped-how-i-added-project-session-and-subagent-attribution-to-my-usage-tracker-5bjm</link>
      <guid>https://dev.to/rehberodhano/claude-usage-jumped-how-i-added-project-session-and-subagent-attribution-to-my-usage-tracker-5bjm</guid>
      <description>&lt;p&gt;The first version of Claude Usage Companion covered the limit bars, a burn-rate forecast, and basic project and model attribution from Claude Code. That told me where I stood. It didn't tell me why.&lt;/p&gt;

&lt;p&gt;What kept happening was this: I'd open Settings → Usage and the weekly bar had jumped. Was it yesterday's refactor? One long Claude Code session? Did a model dominate the week? Did a subagent run much longer than I expected? Or was this just a normal week and I'd forgotten?&lt;/p&gt;

&lt;p&gt;Claude gives you the percentage, not the why. So I kept building. It's at &lt;strong&gt;v0.3.2&lt;/strong&gt; now, still with no backend, no telemetry and no account. Everything stays on your machine.&lt;/p&gt;

&lt;p&gt;(If you want the engineering side, how it captures usage without scraping the page and how the forecast works, I wrote that up in &lt;a href="https://dev.to/rehberodhano/predicting-when-youll-hit-your-claude-usage-limit-without-scraping-the-page-1llh"&gt;the first article&lt;/a&gt;. This one is about what the product does now.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Three places, three questions
&lt;/h2&gt;

&lt;p&gt;The popup stays small on purpose. It shows the Session (5h) and Weekly bars, and says something only when something needs attention: a limit you've already hit, or a forecast that runs out early, like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;At this pace, session runs out ~Thu, 3:40 PM — before it resets.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a bar is rising unusually fast it adds a separate line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session usage is rising faster than its recent pattern.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full dashboard has three tabs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overview:&lt;/strong&gt; am I on track?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity:&lt;/strong&gt; what changed, and where is the usage going?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions:&lt;/strong&gt; find, resume, export and keep old Claude Code sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Activity needs the optional daemon for its content, and Sessions only appears once the daemon is connected. Overview works without it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Overview: am I on track?
&lt;/h2&gt;

&lt;p&gt;Besides the bars, reset times and history, Overview has the forecast, which fits a line through your current usage window and says when it would hit 100%. The dashboard version also tells you how much earlier than the reset that is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;At current pace, reaches limit ~Thu, 5:32 AM — that's 3h 20m before reset.
Switching to a lighter model or pacing back would stretch it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard line shows up for any bar that's at least 10% used and tags a thin forecast as "(low confidence)". The popup headline and the optional pace notification are stricter: they need medium or high confidence and a projection that lands at least 30 minutes before the reset. A warning that fires all the time gets ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sustainable pace
&lt;/h3&gt;

&lt;p&gt;A forecast says where your recent pace leads. The other useful question is how fast you &lt;em&gt;can&lt;/em&gt; go and still make it to the reset. That's plain arithmetic on two numbers Claude reports: what's left, divided by the hours until reset. With 66% left and 5 hours to go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Average usage should stay around 13.2%/hour or lower to last until reset.
Your recent pace is about 40% above that.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second sentence only shows up when the forecast is at least medium confidence, and it's rounded to the nearest 5%, because a fitted rate shouldn't pretend to single-percent precision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anomaly detection
&lt;/h3&gt;

&lt;p&gt;Sometimes the useful signal isn't a forecast, it's "this is climbing much faster than it usually does." The extension compares your current run with your own recent windows: it flags a bar when its current rate is at least twice the median rate of your last few completed windows. It needs a couple of finished windows before it says anything, it stays quiet right before a reset, and it labels itself "early pattern — based on limited history" while the baseline is thin.&lt;/p&gt;

&lt;p&gt;The wording describes behavior, never a cause. A change on Anthropic's side can move the observed rate as much as your own usage can.&lt;/p&gt;




&lt;h2&gt;
  
  
  Usage Inspector: what was running when it spiked?
&lt;/h2&gt;

&lt;p&gt;When an anomaly fires on Overview, there's an inspector under it. It looks at the local Claude Code sessions active during that window and shows the largest local project with its share, the model mix, and the biggest sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Largest local contributor observed during this period:
storefront-api (62% of observed local activity).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;storefront-api&lt;/code&gt; is a made-up name.) Sometimes nothing matches, and then it says so instead of inventing an explanation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Headroom did not observe local Claude Code activity matching this increase.
Usage from claude.ai or other unobserved sources may have contributed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That caveat is the point. The browser sees your account-level usage, and the daemon sees local Claude Code activity. They overlap, but they aren't the same data, since you may also use claude.ai, another machine, or another tool. So the inspector says "observed during this period", never "caused by".&lt;/p&gt;




&lt;h2&gt;
  
  
  Activity: where is it going?
&lt;/h2&gt;

&lt;p&gt;With the daemon connected, Activity breaks Claude Code usage down by project, model and session. There's also a rough "~N% of this week" estimate per project, and I'll repeat the caveat from the UI: the estimate treats all weekly movement as CLI-caused, so it leans high if you also use claude.ai in the same period.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;What changed&lt;/strong&gt; section compares this week with the &lt;em&gt;same elapsed portion&lt;/em&gt; of last week, and only appears when a difference is big enough to matter. If nothing is, the section isn't there at all. What it can say looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project      storefront-api share increased from 18% to 27% compared with
             the same elapsed portion of the previous window.
CLI volume   Observed CLI tokens up 35% compared with the same elapsed
             portion of the previous window.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Subagents
&lt;/h3&gt;

&lt;p&gt;Expand a session and you can see which subagents ran inside it, each with its type, description, model and its own token total. Those totals are shown separately and &lt;strong&gt;never added&lt;/strong&gt; to the session's number. When I looked at real local data, I couldn't tell whether the existing session totals already include subagent activity, and I didn't want to guess at the accounting and show an incorrect sum. A separate, honest number is less tidy than one big total, but it's less likely to be wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sessions: don't lose the good ones
&lt;/h2&gt;

&lt;p&gt;Claude Code keeps session history locally, and by default it cleans old sessions up after 30 days. Since the daemon was already reading those files, a few things became easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full-text search across past sessions&lt;/li&gt;
&lt;li&gt;a copy button for the resume command: &lt;code&gt;cd /path/to/project &amp;amp;&amp;amp; claude --resume &amp;lt;session-id&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Markdown export (images included, and an opt-in for short summaries of tool calls)&lt;/li&gt;
&lt;li&gt;a warning, plus a count on the toolbar icon, for sessions nearing the cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to know a session was about to disappear &lt;em&gt;before&lt;/em&gt; it did.&lt;/p&gt;




&lt;h2&gt;
  
  
  Smaller things that turned out to matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Statusline.&lt;/strong&gt; The npm package includes a script for Claude Code's &lt;code&gt;statusLine&lt;/code&gt; setting. It prints something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session: 34% (resets in 2h 10m) | Weekly: 22% (resets in 76h 10m) | Today: 1.8M Tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session and weekly come from Claude Code's own input (Pro/Max, after your first message in a session), so they work without the daemon. The token total needs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup and restore.&lt;/strong&gt; Usage history lives in the browser, so clearing site data would wipe weeks of it. Settings can export history and settings to a file and restore them later. The daemon connection is deliberately left out of the backup, and a restore only adds snapshots you don't already have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnostics.&lt;/strong&gt; I didn't want to ask anyone to "send me your logs" from a tool that runs next to private conversations. Settings → Diagnostics produces a short summary you can copy: versions, last capture, whether the daemon is reachable, and project and session counts. It never includes your token, account id, or conversation content.&lt;/p&gt;




&lt;h2&gt;
  
  
  What needs the daemon, and what doesn't
&lt;/h2&gt;

&lt;p&gt;The extension on its own handles the bars, history, forecast, sustainable pace and anomaly detection. The daemon is for everything that depends on local Claude Code data: project, model, session and subagent attribution, What changed, the Usage Inspector, session search, resume, export, retention warnings, and the statusline's token total.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @rehberodhano/claude-usage-companion-daemon
claude-usage-daemon &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's optional, binds to &lt;code&gt;127.0.0.1&lt;/code&gt; only, requires a token, rejects requests from regular web pages, and never uploads anything. You'll need an active Claude plan with usage limits, since free accounts have no usage page to read.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chrome / Edge:&lt;/strong&gt; the extension is on the Chrome Web Store (Edge can install the same listing):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chromewebstore.google.com/detail/chjbjdabpficejgogljohhlobfaehepl" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/chjbjdabpficejgogljohhlobfaehepl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Firefox isn't on a store yet, so it has to be built from source. After installing, open Settings → Usage on claude.ai once, or just send a message, so the extension can find your account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source code (MIT):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/RehberOdhano/headroom" rel="noopener noreferrer"&gt;https://github.com/RehberOdhano/headroom&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional Claude Code daemon&lt;/strong&gt; (Node.js 20 or later), also on npm:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@rehberodhano/claude-usage-companion-daemon" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@rehberodhano/claude-usage-companion-daemon&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @rehberodhano/claude-usage-companion-daemon
claude-usage-daemon &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works under the hood&lt;/strong&gt; (network capture, the forecast, localhost security):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/rehberodhano/predicting-when-youll-hit-your-claude-usage-limit-without-scraping-the-page-1llh"&gt;https://dev.to/rehberodhano/predicting-when-youll-hit-your-claude-usage-limit-without-scraping-the-page-1llh&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd like to hear
&lt;/h2&gt;

&lt;p&gt;I'm trying not to add a metric just because I can calculate it. So when your usage spikes, what do you want to know first: which project, which model, which session, whether a subagent was involved, or something else? Bug reports and ideas are welcome on GitHub.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>showdev</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Predict Claude Usage Limits Without Scraping the Page</title>
      <dc:creator>Rehber Odhano</dc:creator>
      <pubDate>Sun, 27 Sep 2026 21:45:14 +0000</pubDate>
      <link>https://dev.to/rehberodhano/predicting-when-youll-hit-your-claude-usage-limit-without-scraping-the-page-1llh</link>
      <guid>https://dev.to/rehberodhano/predicting-when-youll-hit-your-claude-usage-limit-without-scraping-the-page-1llh</guid>
      <description>&lt;p&gt;A usage percentage tells you where you are. It doesn't tell you whether you're in trouble.&lt;/p&gt;

&lt;p&gt;What I actually wanted to know was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;At my current pace, am I going to hit the limit before it resets?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;62% an hour into a window and 62% shortly before the reset are very different situations, and Claude only shows you the number. So I built &lt;strong&gt;Claude Usage Companion&lt;/strong&gt;, an open-source, local-first browser extension that adds the missing context.&lt;/p&gt;

&lt;p&gt;It's at &lt;strong&gt;v0.3.2&lt;/strong&gt; now, but the engineering problem hasn't changed: how do you turn usage snapshots into something you can act on, without scraping Claude's UI or sending your data to yet another backend? This post is about how I approached that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The percentage wasn't enough
&lt;/h2&gt;

&lt;p&gt;Claude's paid plans have two limits: a 5-hour session window and a 7-day weekly window. Both show up as percentages under Settings → Usage.&lt;/p&gt;

&lt;p&gt;On its own, a percentage doesn't say much. Compare these two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Situation A                          Situation B
Session usage:    62%                Session usage:    62%
Time into window: 1 hour             Time into window: 4 hours 30 minutes
Time until reset: 4 hours            Time until reset: 30 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In A you've burned a large share of the allowance very early, and if that pace holds you'll hit the limit long before the reset. In B the number is identical, but you're almost at the reset and probably fine.&lt;/p&gt;

&lt;p&gt;So instead of just "Session usage: 62%", I wanted the extension to say something like this, and only when it's true:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;At this pace, session runs out ~Thu 3:40 PM — before it resets.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The percentage tells you where you are. The pace tells you whether to care.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I didn't scrape the page
&lt;/h2&gt;

&lt;p&gt;The obvious way to build this is to open the usage page, find the percentage in the DOM, parse the text, and save it. It works until a CSS class changes or a component gets redesigned, and then the extension quietly breaks.&lt;/p&gt;

&lt;p&gt;The network responses behind the page are a steadier thing to build on, so the extension watches the responses &lt;code&gt;claude.ai&lt;/code&gt; already receives. Three of them matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/usage&lt;/code&gt;, from a background poll or from the page's own request&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;message_limit&lt;/code&gt; event at the end of a chat reply's stream&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rate_limit_event&lt;/code&gt; entries from Claude Code on the web&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the whole path, from those three sources to what you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  GET /usage ──────────────────┐
  poll, or the page's own call │
                               │     ┌───────────────────────────────┐
  message_limit ───────────────┼───► │ background service worker     │
  end of a chat reply          │     │                               │
                               │     │  1. validate    (zod)         │
  rate_limit_event ────────────┘     │  2. normalize                 │
  Claude Code on the web             │  3. store       (IndexedDB)   │
                                     │  4. forecast                  │
                                     │  5. alert, badge, popup       │
                                     └───────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page hook only observes. Claude gets its original response, untouched. The background poll is separate: every 5 minutes an alarm requests &lt;code&gt;/usage&lt;/code&gt; using your existing claude.ai session. claude.ai's own frontend only asks for that endpoint when you open Settings → Usage, and I didn't want the bars going stale in between.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chrome's isolated world makes this awkward
&lt;/h2&gt;

&lt;p&gt;Chrome content scripts run in an isolated JavaScript environment. That's good for security, but it means a normal content script can't replace the page's &lt;code&gt;window.fetch&lt;/code&gt;. To watch Claude's network calls I need two small scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; claude.ai tab
┌───────────────────────────────────────────────────────┐
│                                                       │
│  MAIN world (the page's own realm)                    │
│  ┌─────────────────────────────────────────────────┐  │
│  │ hook                                            │  │
│  │  · wraps window.fetch                           │  │
│  │  · reads a clone, the page gets the original    │  │
│  │  · drops everything except usage events         │  │
│  └───────────────────────┬─────────────────────────┘  │
│                          │ window.postMessage         │
│  ISOLATED world          ▼                            │
│  ┌─────────────────────────────────────────────────┐  │
│  │ relay                                           │  │
│  │  · has browser.runtime access                   │  │
│  │  · passes payloads along, no parsing            │  │
│  └───────────────────────┬─────────────────────────┘  │
│                          │                            │
└──────────────────────────┼────────────────────────────┘
                           │ runtime message
                           ▼
                background service worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook is small on purpose, and it has one rule: &lt;strong&gt;a bug in my extension must never break claude.ai.&lt;/strong&gt; Simplified, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;originalFetch&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;handleResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nf"&gt;getRequestUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Losing a usage sample is acceptable.&lt;/span&gt;
    &lt;span class="c1"&gt;// Breaking Claude is not.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;response.clone()&lt;/code&gt;. The page gets the original, and the extension reads the copy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Some of the data arrives inside a stream
&lt;/h2&gt;

&lt;p&gt;Chat replies are streamed, and the &lt;code&gt;message_limit&lt;/code&gt; event I want is buried near the end of that stream. So it isn't a simple &lt;code&gt;await response.json()&lt;/code&gt;. The body is a &lt;code&gt;ReadableStream&lt;/code&gt;, and the extension has to read chunks and rebuild complete SSE frames from them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SseFrameBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handleFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handleFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buffer matters because network chunks don't respect message boundaries. One SSE event can be split across two chunks, or several can arrive in one, so parsing line by line straight off each chunk isn't reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Throwing away what I don't need
&lt;/h2&gt;

&lt;p&gt;Some of these responses carry far more than usage data, and I decided early that being able to see something isn't a reason to keep it.&lt;/p&gt;

&lt;p&gt;Claude Code on the web is the clearest case. Its events endpoint returns &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;assistant&lt;/code&gt; entries alongside the &lt;code&gt;rate_limit_event&lt;/code&gt; entries I'm after. The filter runs inside the page's own context, &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;postMessage&lt;/code&gt; fires, so conversation text never reaches the extension at all, not even briefly. The chat stream gets the same treatment: only &lt;code&gt;message_limit&lt;/code&gt; frames are forwarded, and the rest is ignored.&lt;/p&gt;

&lt;p&gt;I don't want this to become a conversation recorder. Keeping that boundary narrow also makes the privacy story easy to explain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Treating undocumented data as unstable
&lt;/h2&gt;

&lt;p&gt;None of this was designed as a public API, so I assume it can change under me. Fields appear and disappear, objects turn into &lt;code&gt;null&lt;/code&gt;, new keys show up. Every payload goes through runtime validation before anything is stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UsageSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;logValidationFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Those names are illustrative, not the real identifiers.) A few rules came out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unknown fields don't invalidate a payload.&lt;/strong&gt; A new key in Claude's response isn't a reason to throw it away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional means optional.&lt;/strong&gt; If I haven't seen a field in every capture, nothing depends on it being there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad input gets skipped, not guessed at.&lt;/strong&gt; I'd rather show nothing than a confident wrong number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Defensive parsing makes the integration more resilient. It doesn't turn an undocumented interface into a stable one, and if the format changes enough, the extension will go quiet until I ship a fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The forecast is boring math
&lt;/h2&gt;

&lt;p&gt;Once I had a history of snapshots, I needed to estimate where the current run was heading. I thought about something fancier and decided against it. The forecast fits a straight line (ordinary least squares) through recent samples and solves for where it reaches 100%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 100% │                                 ·
      │                              ·
  80% │                           ·
      │                     ·  ·
  60% │                  ·
      │               ·
  40% │         ●  ●
      │      ●
  20% │   ●
      │●
   0% │
      └──────────────────────────────────────▶ time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filled dots are real samples, and the small dots are the line projected forward until it hits the top.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resets break a naive fit
&lt;/h2&gt;

&lt;p&gt;Windows reset, and when they do the percentage falls back toward zero. A history might look like 72%, 79%, 86%, then 4%, 10%, 18%. A single line fitted across the whole history would be nonsense, because the reset breaks it in the middle. The fit has to start after the drop, and the projection should come only from the new run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 100% │        ┊                                          ·
      │      ● ┊                                       ·
  80% │   ●    ┊                                    ·
      │●       ┊                              ·  ·
  60% │        ┊                           ·
      │        ┊                     ·  ·
  40% │        ┊                  ·
      │        ┊               ·
  20% │        ┊         ●  ·
      │        ┊      ●
   0% │        ┊   ●
      └────────────────────────────────────────────────────────▶ time
               reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So only the current run counts. Starting from the newest sample, walk backward until the percentage drops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;runStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;runStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runStart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentRun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That way the fit only sees samples from the current window. (In the real code this splitting rule lives in one shared helper, because anomaly detection needs the same reset boundaries.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not something smarter?
&lt;/h2&gt;

&lt;p&gt;Because real usage is lumpy. You prompt hard for twenty minutes, read code for forty, sit in a meeting, then start a big session. Nothing inside the extension knows what you'll do thirty minutes from now, and a fancier model would mostly add the look of precision without knowing any more.&lt;/p&gt;

&lt;p&gt;That's why the wording is always "at your recent pace". The forecast doesn't predict what you'll do, it extrapolates what you've already been doing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Confidence over precision
&lt;/h2&gt;

&lt;p&gt;A forecast built on two samples shouldn't look as trustworthy as one built on a longer history, so every forecast carries a deliberately cheap confidence label:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;low:&lt;/strong&gt; fewer than 3 samples in the current run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;medium:&lt;/strong&gt; 3 or more samples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;high:&lt;/strong&gt; 5 or more samples spanning at least an hour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a rough proxy for how much to trust the line, not a statistical confidence interval. It's also why I never show "You WILL run out at exactly 3:47 PM". Something like "session runs out ~Thu 3:40 PM — before it resets" gives you what you need to make a call without pretending to know the future.&lt;/p&gt;




&lt;h2&gt;
  
  
  Knowing when to stay quiet
&lt;/h2&gt;

&lt;p&gt;This ended up mattering as much as the math. If an extension warns you all the time, you stop reading the warnings, so a forecast shouldn't automatically become an alert. The popup headline and the opt-in pace notification only fire when all three of these hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the bar is at least &lt;strong&gt;10%&lt;/strong&gt; used (a forecast of "limit in 141 hours" at 3% used is noise)&lt;/li&gt;
&lt;li&gt;the forecast has &lt;strong&gt;medium or high&lt;/strong&gt; confidence&lt;/li&gt;
&lt;li&gt;the projected run-out lands at least &lt;strong&gt;30 minutes before&lt;/strong&gt; the window resets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a projection of 4:58 PM against a 5:00 PM reset gets no drama, since that's effectively fine. A projection of 2:40 PM against the same reset does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sustainable pace
&lt;/h2&gt;

&lt;p&gt;Forecasting asks when you'll hit the limit at your recent pace. There's a second useful question: how fast can you use what's left and still make it to the reset?&lt;/p&gt;

&lt;p&gt;Say you have 66% remaining and 5 hours until reset. This one isn't a forecast, it's plain arithmetic on two numbers Claude reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(100 − percent used) ÷ hours until reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here that's 66 ÷ 5 ≈ 13% per hour, the rate at which you'd use up the remaining allowance exactly as the window resets. The extension compares it with your recent pace, but only when the forecast is medium or high confidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recent pace:       high
Sustainable pace:  lower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to read a chart to see that you're burning faster than you can afford. This is part of the current version as well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Account usage and local Claude Code activity aren't the same data
&lt;/h2&gt;

&lt;p&gt;The browser can see account-level usage. People also asked what they were doing in Claude Code that was contributing to it, and a browser extension can't look inside your terminal. Claude Code does keep session data locally, though, so Claude Usage Companion has an &lt;strong&gt;optional local daemon&lt;/strong&gt; that reads it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; your browser                             your machine
┌──────────────────────────┐            ┌─────────────────────────────┐
│ extension                │  HTTP to   │ daemon (Node + Hono)        │
│  · usage history         │  127.0.0.1 │  · reads Claude Code's      │
│  · forecasts, dashboard  │ ─────────► │    session logs             │
│                          │  + token   │  · shells out to            │
│                          │ ◄───────── │    ccusage --offline        │
└────────────▲─────────────┘            └──────────────┬──────────────┘
             │ network capture                         │ read-only
             │                                         ▼
         claude.ai                      ~/.claude/projects/**/*.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The daemon can derive projects, models, sessions, token activity and subagent activity. What it can't do is tell you how that activity relates to your account totals, and I don't want the UI to imply otherwise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlation isn't causation
&lt;/h3&gt;

&lt;p&gt;Suppose weekly usage jumps, and around the same time the daemon sees a big local Claude Code session. It's tempting to blame the session. But you may also have used claude.ai in the browser, used Claude somewhere else, switched models, or generated more output in that window. The data supports "this local activity happened then", not "this caused the jump".&lt;/p&gt;

&lt;p&gt;So the UI treats three kinds of values differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reported by Claude    session usage, weekly usage, reset times
Observed locally      Claude Code projects, models, sessions, subagents, token activity
Derived               forecast, sustainable pace, anomaly detection, contribution estimates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They don't have the same provenance, so they shouldn't look like they do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Localhost isn't automatically private
&lt;/h2&gt;

&lt;p&gt;It's easy to assume that anything on &lt;code&gt;127.0.0.1&lt;/code&gt; is private, but any website you open can try to send requests to services listening on localhost. So the daemon doesn't lean on the loopback address alone. Every request has to get past a few checks, in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; request                                             result
 ─────────────────────────────────────────────────   ───────────────────────
 a website (http/https origin), even with a token    403 forbidden_origin
 anyone, no token                                    401 unauthorized
 your extension + token                              ok
 curl or the statusline script (no Origin) + token   ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full list of protections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;binds to &lt;code&gt;127.0.0.1&lt;/code&gt; only&lt;/li&gt;
&lt;li&gt;a bearer token on every route, compared in constant time, with the token file readable only by you&lt;/li&gt;
&lt;li&gt;any &lt;code&gt;http(s)&lt;/code&gt; page origin rejected outright, even with a valid token&lt;/li&gt;
&lt;li&gt;no unauthenticated routes except &lt;code&gt;/health&lt;/code&gt; and the one-time &lt;code&gt;/pair&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;no conversation content in logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pairing means you never copy a token by hand. The extension asks the daemon for its token once, right after &lt;code&gt;install&lt;/code&gt;, and the daemon then locks that endpoint, so every later request is refused. (If you reinstall the extension, run &lt;code&gt;claude-usage-daemon install&lt;/code&gt; again to open a fresh pairing window.)&lt;/p&gt;

&lt;p&gt;I kept the daemon narrow. It's stateless: your history lives in the extension's local storage, and the daemon only writes its own token file, a pairing marker, and its background-service registration. It never writes into your projects, and I don't want it to grow into a general-purpose local API server. Features like session search and export do read session content, but only locally and on demand, and nothing is sent anywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  The privacy model is boring on purpose
&lt;/h2&gt;

&lt;p&gt;There's no Headroom backend between you and Claude, no telemetry, no product account, and no analytics service receiving your history. The browser side works with Claude data your signed-in session can already see, and the optional daemon works with files on your own machine. Everything it stores stays local. For a tool that watches your usage, I think boring infrastructure is a feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it has become
&lt;/h2&gt;

&lt;p&gt;Early versions already showed the limit bars and a burn-rate forecast. The current version tries to answer more questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where am I now?&lt;/li&gt;
&lt;li&gt;Am I likely to hit the limit before reset?&lt;/li&gt;
&lt;li&gt;Is my current pace sustainable?&lt;/li&gt;
&lt;li&gt;Is this usage pattern unusual?&lt;/li&gt;
&lt;li&gt;What changed recently?&lt;/li&gt;
&lt;li&gt;Which Claude Code projects and models were active?&lt;/li&gt;
&lt;li&gt;Which sessions and subagents ran?&lt;/li&gt;
&lt;li&gt;Can I find and resume an older Claude Code session?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That deserves its own article, so I'm not turning this one into a v0.3.2 changelog. This post is about the foundation those features sit on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;It's a TypeScript monorepo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
├── shared/      schemas and shared usage models
├── extension/   WXT + React + IndexedDB
└── daemon/      Node.js + Hono
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension handles capture, local history, forecasting, alerts and the dashboard. The daemon is optional and only handles local Claude Code visibility. Both live in the same open-source repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chrome / Edge:&lt;/strong&gt; Claude Usage Companion is on the Chrome Web Store (Edge can install the same listing):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chromewebstore.google.com/detail/chjbjdabpficejgogljohhlobfaehepl" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/chjbjdabpficejgogljohhlobfaehepl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Firefox isn't on a store yet, so it has to be built from source (the repo explains how). After installing, open claude.ai's &lt;strong&gt;Settings → Usage&lt;/strong&gt; page once, or just send a message, so the extension can detect your account. From then on it keeps itself up to date in the background. You'll need an active Claude plan with usage limits, because free accounts have no usage page to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source code (MIT):&lt;/strong&gt; &lt;a href="https://github.com/RehberOdhano/headroom" rel="noopener noreferrer"&gt;https://github.com/RehberOdhano/headroom&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional Claude Code daemon&lt;/strong&gt; (needs Node.js 20 or later):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @rehberodhano/claude-usage-companion-daemon
claude-usage-daemon &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension works fine without the daemon. Install it only if you want the local Claude Code features.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd like to hear
&lt;/h2&gt;

&lt;p&gt;This started because I wanted a better answer than "62% used". I wanted to know whether I was actually in danger of hitting the limit before the reset. That question led to forecasting, sustainable pace, anomaly detection, Claude Code attribution, session tools, and a fair number of rabbit holes.&lt;/p&gt;

&lt;p&gt;I don't want it to turn into a dashboard full of numbers just because they can be calculated. So if you use Claude regularly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What's the one piece of usage information you wish Claude showed you directly?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Feedback, bug reports and feature ideas are all welcome on GitHub.&lt;/p&gt;




</description>
      <category>claude</category>
      <category>showdev</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
