The hard part: you can't see the network from a content script
Extensions run content scripts in an isolated world — you get the DOM, but not the page's own `window.fetch`. The requests worth watching happen in a window the extension is not allowed to touch.
So the work happens on both sides of that wall, and the two halves talk to each other over `postMessage`.
- claude.ai API: What both halves ultimately read — a bootstrap call to resolve the organisation, then the usage endpoint carrying the window counters. The same API whether a browser is asking or the CLI is.
- claude.ai: An app I don't control and have no contract with. Its own requests carry the numbers, so they have to be observed without its cooperation. No versioning and no guarantees. A redesign upstream can change response shapes with no warning, and there is nobody to appeal to.
- fetch wrapper: Injected into the page's own world, where the real calls happen, and it wraps window.fetch there. Capturing it before any framework can wrap it first is the whole trick — get the ordering wrong and you instrument React's wrapper instead of the network. It also patches history.pushState and replaceState, because single-page navigation never fires a page load to hook onto.
- postMessage: A content script cannot reach the page's window.fetch, and page code cannot reach extension APIs. Neither half can do the job alone, so they talk across the boundary with a small request/response protocol.
- Content script: Where extensions are designed to run: full DOM access, extension APIs, and no access to the page's own JavaScript. Which is precisely the wrong side of the wall for watching the network.
- Tokenizer: Counts tokens on the machine. Sending conversation text to a service to find out how large it is would leak the exact thing being measured — an absurd trade for a usage meter.
- Usage panel: Tokens for the current message, whether the context was read from cache, and how much of the 5-hour and 7-day windows is gone — with a countdown to the reset.
- Cookie store: Chrome encrypts with AES under a PBKDF2-derived key and holds a lock on the live file, so it has to be copied before it can be read. Firefox keeps SQLite. Safari has its own binary layout. When none of the three can be read — common on macOS, or when the session is memory-only — setup falls back to a guided manual paste rather than failing.
- Status line: Calls those same endpoints itself with the borrowed session, then renders the numbers into the Claude Code status line. It never touches the extension — a second, independent route to the same data.
Trade-offs
Parsing defensively rather than trusting the shape. Every field is read as something that might not be there, so when a response changes underneath me the panel drops the number it can no longer read and keeps showing the rest. The alternative — assuming the shape and throwing — turns somebody else's deploy into a broken extension.
Counting locally rather than on a server. The `o200k_base` tokenizer is vendored into the bundle, so no conversation content ever leaves the machine. It costs bundle size and nothing else — and the alternative would have leaked exactly what the tool exists to measure.
Zero runtime dependencies. Nothing to audit in a supply chain, on something that sits on top of your chat and reads every response.
A fallback instead of a hard failure. Cookie auto-detection could not be made reliable across three browsers and two operating systems, so it degrades to a guided paste. An auto-detect that works on four setups out of five and hard-fails on the fifth is worse than one that always finishes — the person on the fifth has no idea whether they're holding it wrong.