Skip to main content

QuotaLens

CI

Claude tells you what is consuming your quota right now, and then forgets. QuotaLens remembers. The short version, with the dashboard, is at quotalens.com; the install page is quotalens.com/install.

It is a local, self-hosted monitor for Claude Pro and Max subscription usage. It polls your account every minute, keeps the series in a SQLite file you own, and leads with the question you open it to ask: will this session window run out before it resets, and when. When something surprises you, the record is still there — which five-hour session, how steep the climb, the minute it started.

macOS, Linux and Windows, each tested on every push — with one caveat about Windows worth reading before you rely on it, in Platforms. Binds loopback, keeps your cookie in the OS keychain, phones nothing home. MIT.

Unofficial. It uses undocumented claude.ai endpoints and only observes: it cannot raise, extend or bypass a limit. Not affiliated with or endorsed by Anthropic, and the endpoints may change without notice. The longer version is in The Terms, stated plainly.

Status: v2.0.

Quick start

pipx install quotalens      # or: uv tool install quotalens
quotalens auth              # paste your claude.ai session cookie once (stored in the OS keychain)
quotalens probe             # one fetch, prints raw + parsed output for debugging
quotalens start             # background: pid file + rotating log in the data directory
open http://127.0.0.1:8787   # the dashboard
quotalens status            # exit 0 healthy, 1 not running, 2 stalled
quotalens logs -f
quotalens stop
curl 'http://127.0.0.1:8787/api/quota/current'

Then, as you need them:

curl 'http://127.0.0.1:8787/api/budget'                    # the weekly limit, in session windows
curl 'http://127.0.0.1:8787/metrics'                       # Prometheus, hand rolled
curl 'http://127.0.0.1:8787/api/export.csv?table=quota'    # or export.json
curl 'http://127.0.0.1:8787/api/events'                    # threshold crossings, anomalies
curl 'http://127.0.0.1:8787/api/weeks'                     # one row per weekly reset
curl 'http://127.0.0.1:8787/api/pace'                      # where this week is heading, with its spread
quotalens profiles list --json                             # every profile, its port, running or not
quotalens prune --dry-run                                  # what retention would remove
quotalens forget                                           # session windows, and their ids

Set QUOTALENS_WEBHOOK_URL to get one POST when the burn rate crosses QUOTALENS_BURN_ALERT points per hour (default 20), and one when it falls back. It feeds ntfy, Discord, Slack, Pushover or Home Assistant. The body:

{
  "event": "burn_alert",
  "profile": "default",
  "ts": 1788456405,
  "rate_pts_per_hour": 42.5,
  "threshold_pts_per_hour": 20.0,
  "headroom_pct": 37.0,
  "session_resets_at": "2026-09-03T18:00:00+00:00",
  "text": "Burn rate 42.5 pts/hr crossed the 20 pts/hr threshold, 37% of the session left.",
  "url": "http://127.0.0.1:8787/"
}

profile is the local label you chose, so a receiver watching two of them can tell them apart. There is no organisation id, no account identifier and no cookie in it, by design.

Settings that survive a restart

Flags and QUOTALENS_* variables still work and still win. What is new is a place to put a choice so you do not retype it:

quotalens config set port 9123     # then `start` binds there with no flag
quotalens config get port
quotalens config unset port
quotalens config list              # every setting, and where its value came from

config list is the one worth knowing. It prints the merged result and names the layer each value came from, because "why is it not the port I set" is a precedence question:

file: ~/Library/Application Support/quotalens/config.json
  port          9900    flag
  interval      60      default
  poll_enabled  True    default

Precedence, highest first: CLI flag → QUOTALENS_* → config.json → built-in default. The file lives beside the database and is profile-suffixed the same way (config-work.json). It is plain JSON, written atomically, and it never contains your cookie — that stays in the OS keyring, so the config file is safe to paste into an issue.

Two settings are deliberately CLI-only. The port is the address of the page the settings live on, so changing it from that page means the response never arrives. The cookie is a secret and belongs in the keyring. Everything else can be set either way.

Settings, from the dashboard

The gear in the header opens /settings: poll interval, burn thresholds, retention, webhook, notifications and the vendor status row. It is a plain form that works with JavaScript switched off, validates on the server with the same rules the CLI uses, and shows the real error beside the field rather than quietly clamping a bad value. Each field says whether it takes effect on the next poll or needs a restart.

The port, the database path and the cookie are shown read-only, with the command that changes each. The port is the address of the page you are looking at.

Desktop notifications are there and are off by default. Turn them on in the panel and pick up to three usage levels; a banner fires once when a window crosses each. Off by default because it is three OS code paths — osascript or terminal-notifier on macOS, powershell on Windows, notify-send on Linux — and it is dead under a systemd user unit with no session bus, which is how this is meant to run. The panel says so, with the reason, rather than offering a switch that does nothing; the webhook is one code path that works everywhere and is the right default for an unattended instance.

Vendor status

A row per vendor in the side panel — Claude, Gemini, OpenAI — reading each vendor's own status page. The whole row is a link to that page, in a new tab.

This, the daily update check, and the webhook if you set one, are the only outbound traffic to anyone other than your provider: one plain GET per vendor every five minutes, no cookie, no identifier, no query string. The update check is one GET to pypi.org a day, described under How it reaches claude.ai. It is on by default and quotalens config set status_row false turns it off, which stops the requests rather than hiding the row.

Gemini is not covered. Google publishes no status API for AI Studio or the Gemini API — its status page is a JavaScript app with no JSON behind it, and Google Cloud's product feed covers Vertex surfaces, which have their own availability record. A row that could only ever say "unable to check" earns nothing, so there is no Gemini row. It becomes four lines the day Google ships a feed.

The marks beside each vendor's name are that vendor's own brand asset, shipped unmodified to identify their service. They belong to their owners, and QuotaLens is not affiliated with, endorsed by, or associated with any of them — Anthropic included, which is the whole reason for saying so here.

Two accounts

A profile is a second account. It gets its own keyring entry, its own database, its own port and its own pid file, so the two never see each other:

quotalens --profile work auth
quotalens --profile work start        # a derived port, printed by `start`
quotalens --profile personal auth
quotalens --profile personal start
quotalens --profile work stop         # leaves the personal one running

quotalens profiles list shows every profile, its port and whether it is running.

Two accounts is two processes and two bookmarks, not an account picker inside one process. The port is derived from the name and is the same on every run, so start, serve and status all print the URL they landed on — you should never have to work out which port a profile got. If something else already holds it, the error names the port, the profile and the --port flag that settles it. QUOTALENS_PROFILE works too, for a service unit.

Every profile must be an account that is yours. Anthropic's Consumer Terms (section 2) forbid sharing your login or credentials with anyone else or making your account available to anyone else, so a profile holding someone else's cookie breaks their terms, not just yours. And the Usage Policy forbids using several accounts to get around a product's limits. QuotaLens shows each account's own state; it does not suggest which account to switch to, and it is not meant for rotating work across accounts.

Running it as a service

One command makes it start on its own every time you log in, and one undoes it:

quotalens service install     # start at login, from now on
quotalens service status      # is it set up, and is it running?
quotalens service uninstall   # stop doing that, and remove what was written

What that installs depends on the OS:

Platform What is registered Restarts if it crashes
macOS LaunchAgent, RunAtLoad and KeepAlive yes, launchd
Linux systemd user unit, Restart=on-failure yes, systemd
Windows Task Scheduler task QuotaLens, trigger "at log on" no, next login

Every file written and command run is printed, so it can be undone by hand. The installed command carries an explicit --data-dir, so the service collects into the same place your shell does. serve stays the foreground command the service manager execs, and start/stop/status/logs still work alongside.

On Linux the unit only runs while you are logged in unless you enable lingering; the installer prints the exact loginctl enable-linger command. On Windows the task starts at login, not at boot, and only for your account.

A background agent reading the OS keychain may prompt on first run or be refused; quotalens status then says "keyring" specifically rather than "no data".

The weekly limit, in windows you can plan with

"Weekly is at 93%" is not a number you can act on. The dashboard puts the same fact under the weekly meters in the unit the work actually arrives in:

Limit                 Left   Full sessions left   At your typical session   Each full session costs
Weekly — all models     6%                  0.5   0.5  at 89% used         12 pts  (10–15, from 6 sessions)
Weekly — Fable          0%            none left   none left

There is time for 13.6 more sessions before this resets Mon 09:30, and budget
for 0.5 — the budget is what runs out.
Weekly — Fable is spent, so none of the 6% left on Weekly — all models can be
used on it.

The note under the table says which of the two constraints binds, because that is the finding: half a session of budget against thirteen sessions of wall clock means rationing, not racing.

The cost of a window is measured, not assumed. Every complete session window in your history carries both its own consumption and what it cost each weekly limit, so the ratio is an observation about how you use models. The median is the estimate and the range beside it is the spread, because the model mix moves it — in one real history the same 100% window cost between 9.6 and 14.8 points.

Windows that would poison the ratio are excluded, strictly: one still running, one only partially observed, one the weekly limit reset inside, one too small to divide, and one where the limit was already at its cap and so could not move. Below five usable sessions it prints the reason in the cell instead of a number: "Needs 5 complete session windows to estimate the cost of one; 3 so far." An em dash cannot be told apart from "the answer is nothing", and those are opposite facts to plan against. It does not lower the threshold to make a number appear — a confident "3.2 sessions left" drawn from two observations is worse than none.

It is on /api/budget and in /metrics as quotalens_weekly_windows_remaining, quotalens_weekly_window_cost_points and quotalens_weekly_clock_windows_remaining.

Weeks, and what a session cost in each

The budget table answers "how many sessions are left this week". It cannot answer the next question: is a session costing me more of the week than it used to? For that you need last week's answer, and the week before's, and the vendor's page forgets them at every reset.

So QuotaLens records each weekly reset as it happens (Monday 01:00 UTC, give or take a second) and keeps a ledger under History, one row per week:

Week              Closed at      One full session used          Used   Left unused   Full sessions at reset   Mostly
21 Sep – 28 Sep   28 Sep 06:30   9% of week · usually 8–10%     99%    1%            8.5                      Claude Code 64%
14 Sep – 21 Sep   21 Sep 06:30   9% of week · usually 8–11%     100%   0%            8.0                      —
7 Sep – 14 Sep    14 Sep 06:30   13% of week · usually 10–15%   99%    1%            8.2                      —

One full session used is how much of the week a full five-hour session cost that week: the median over the week's usable sessions, with its usual range, the same estimate the budget table makes, frozen when the week closed. Full sessions at reset is what the budget table said in the minute before the reset. Mostly is the surface that used the most of that week (see Where the quota went); a dash for weeks before that split was collected.

Under the table, one sentence compares the two newest complete weeks: "A full session used 9% of the week, against 9% the week before. The usual ranges overlap, so no change can be called." When the ranges do not overlap it says which way the cost moved. It never says a limit was raised: a session getting cheaper relative to the week is either the weekly pool growing or the session pool being re-weighted, and the ratio cannot tell those apart.

If your database predates this, the ledger is backfilled from the readings already stored on the next start. GET /api/weeks returns the same rows, table=weeks exports them, and every reset is also a week_reset event.

The Fable meter is half the pool

On plans with a Fable meter, Anthropic's help centre says Fable models may use up to 50% of your weekly limit. So that meter's 100% is half the weekly pool, not an empty account, and the meter says half of weekly pool beside it. It does not turn the page critical on its own.

That is the vendor's rule, not a measurement, so QuotaLens checks it on every poll against two things that must hold if it is true: Fable can never be more than twice the all-models figure at any reading, and over a session window it can never climb more than twice what the all-models meter did (each with a point of slack for whole-percent rounding). If either fails, a subcap_violation event is recorded and the note under the budget table says the rule may not hold for your account.

Where the week is heading

Under the budget table, one line projects where Weekly — all models lands at the reset: "At this pace the week ends near 93% (81–100%)", or "At this pace the week runs out around Thu 14:00 (Wed 22:00 – not before the reset)". It always carries its range, and it is labelled an estimate.

The projection is the week so far plus what your recent complete weeks went on to use from the same hour to their reset. That was chosen by back-testing three methods against real weeks; the straight line from the week so far missed by three times as much, because most weeks start slowly and finish hard. It stays hidden for the first 24 hours of a week and until there is one complete week to learn from.

On the this week range the chart also draws last week's all-models line, dashed and muted, under this week's, so "am I ahead of last week" is a glance. The vs last week chip turns it off.

After Weeks, When you use it is a 7 × 24 grid: weekday by local hour, each cell the average all-models points gained in that hour over the last four complete weeks. It is grey on purpose; nothing about using your quota at 3 am is an alarm. An hour the collector missed is left out of the average rather than counted as zero. It needs two complete weeks and says so until then.

GET /api/pace and GET /api/heatmap return both.

Where the quota went

Since late September 2026 the usage payload carries Anthropic's own split of the week's usage by surface: Claude Code, Chats, Cowork, Other. QuotaLens stores it (a row only when a share changes) and shows it as one stacked bar and a table:

Surface       Share of this week   ≈ of the week's limit
Claude Code   71%                  ≈ 44%
Chats         22%                  ≈ 14%
Cowork         7%                  ≈ 4%

The shares are the vendor's and add up to the week's usage, not to the limit. The second column multiplies each share by the all-models meter to say roughly how much of the limit it took; that is an estimate and is labelled as one. For which skill, subagent or MCP server inside Claude Code, claude /usage still does it better, and the section says so in one line.

GET /api/breakdown returns the split, table=surfaces exports its history, and the Weeks ledger's Mostly column is its top surface at each week's close.

The cloud session credit

Some accounts carry a promotional dollar credit for Claude Code cloud sessions ($250 on Max, $100 on Pro in September 2026), with an expiry date. It arrives in the same payload as the quota windows, but it is not one: it never resets and is not part of any weekly limit. QuotaLens keeps it out of the chart, the budget, Weeks and the boost detector, and shows it on its own under Usage credits:

Cloud session credit    $21.29 of $250
$228.71 left · expires 4 Nov

Seven days and again one day before the expiry, while money is left on it, it records an event and, if desktop notifications are on, sends one. After the expiry it reads "expired" for a week, then leaves the page. GET /api/credits returns it in dollars, table=credits exports it, and the events are credit_grant_seen and credit_grant_expiring.

Your plan

The header shows the plan as a muted label after the name, QuotaLens Max 5x (or Max 20x, Pro, Team, Enterprise, Free), and About has a Plan row. The plan is not in the usage payload; it comes from /api/bootstrap, asked on the first good poll after start and then once a day. Only three fields of that response are kept: the organisation's capabilities, its rate-limit tier and its billing type. No names, no emails, no ids. A failed fetch keeps the last plan it knew and never fails a poll.

It is informational only. Nothing on the page changes with the plan: the meters are whatever the readings contain, because a reading cannot be out of date and a plan label can. A Pro account, which has no Fable meter, simply shows none. /api/health carries it as plan.

When a limit is raised

Anthropic can raise a limit mid-window: the level falls without the window resetting. QuotaLens records it, because a tool that only shows the current value loses the fact by the next day.

08:09 — Weekly — all models fell 98% -> 0% with no reset. Limit raised.

The event is the record; the chart marks the moment, the affected meter says "boosted 08:09" while that window is still running, and the History row it fell in is badged. A boost is information, not a state, so it takes no colour of its own.

If your database predates this feature, quotalens rescan finds boosts in the readings already stored and records them. It is safe to run twice: a boost already recorded is skipped, never rewritten.

It also has to be kept out of the arithmetic. A boost inside a session window makes that window's weekly change consumption minus the raise, and that number is what the cost estimate behind "full sessions left" is built from — so a boost would quietly make the weekly budget optimistic. The payload carries no ceiling anywhere, only utilization, so the size of a raise cannot be recovered: an observed fall is consumption and boost together with nothing to separate them. The window is therefore excluded from the estimate and the History row says why. A window that cannot be costed is honest; one costed wrong is not.

When a window is not running

A five-hour window expires and, until the next message, no window is open. The reading from the window that just closed is not a current reading of anything, so nothing shows it: the hero says so, the meter's value is removed, its footer says ended 14:00 rather than "resets 14:00", and the ring and the favicon go to the empty track. The chart shades the gap as "no session" instead of running the trace flat to the right edge.

The rule is one line — never present a session percentage as current once its window's reset time has passed — and it is implemented in window_has_lapsed and in compute_runway. Two places, not one: the meters, /api/quota/current and /metrics reach it through the first, the hero through the second. Consolidating them is open work, and until it is done a change to either has to be made to both.

Staleness is also tracked per window, not only per collector. A block can stop arriving inside an otherwise healthy payload, and a healthy collector is not evidence that every meter on the page is current; a window whose own reading has gone unrefreshed for three poll intervals is withheld like any other unknown.

/api/quota/current says all of this in fields rather than leaving a consumer to infer it. pct is null unless the reading is current, the value moves to last_pct, and current, lapsed, stale and window_open say which reason applies. /metrics reports NaN for the same cases, never a stale number somebody would alert on.

The dashboard

The figure it leads with is how much of the session window is left. Below it: one meter per quota window with the API's own severity, a 24-hour chart of every window with resets drawn as gaps, and the extra-usage spend computed from minor units with the payload's exponent, never clamped at 100%.

States are honest by construction. If the collector has not succeeded in three poll intervals, the cookie was rejected, or the response could not be parsed, every value is replaced by an em dash and the frame changes, so a stale page never looks like a healthy one showing low usage. If the browser loses the server, the same treatment applies from CSS alone.

The dashboard is interrogable and every view is a URL: range presets from 15 minutes to all, drag on the chart to zoom to any window (double-click resets), click a series' end label to hide it, pick the burn-rate lookback, set auto-refresh, and force a poll (one per 10 seconds). All of it works with JavaScript disabled as plain links and forms.

Above the chart a row of chips picks the series: All, Session, Weekly all, and one per model meter your plan has. The choice is remembered in the browser. The this week and last week ranges run Monday 01:00 UTC to Monday 01:00 UTC, the vendor's own weekly reset, not your calendar week.

The top of the page answers the one question the dashboard exists for: will the session window run out before it resets? Beside the burn rate sit a ticking countdown to the reset, the headroom left, and the sustainable rate, the points per hour you could burn from now to the reset without exhausting it. The verdict sentence changes with the situation: "Exhausted at 21:04, 2h 09m before reset", "At this rate you finish with 37% unused", or "Flat for 4m. 37% left, resets in 2h 14m". Once five complete windows exist it adds how the projected finish compares with your median window. Five bars beneath show the points consumed in each hour of the window. The chart's default range is the current window from start to reset, with now inside it, hourly separators, and a dashed projection at the current rate that turns critical where it crosses 100%.

Below the chart, the history table lists your session windows, derived from the API's own resets_at values: when it jumps forward a new session started.

Where a session starts is an inference. Anthropic documents that the session limit resets every five hours and has never published how the window is anchored, so QuotaLens infers the start as the reset time minus five hours. The evidence for it is that the server recomputes resets_at on every call and only the sub-second part moves, which is what a fixed anchor looks like. Rather than hedge, the collector checks the inference on every poll: if a reset time ever moves forward by less than five hours without the percentage dropping, the model is wrong, and QuotaLens records it and says so on the page. Anything derived from session history — the table, the hour strip, the auto range, the coverage badge — rests on that inference.

Each row shows the peak utilisation reached in the window, how far each weekly limit moved during it, a sparkline of its shape, and how much of it was observed. Sort by consumption to find the expensive session, click it, and the chart shows exactly that window. On the chart, session starts are vertical rules and spans with no session running are shaded flat, which is a different mark from the hatched spans where the collector was not running.

How it reaches claude.ai

claude.ai sits behind Cloudflare bot protection that fingerprints the TLS handshake. A plain Python HTTP client is served a challenge page even with a valid session cookie, so QuotaLens talks to claude.ai through curl_cffi, which impersonates a browser's TLS and HTTP/2 fingerprint. If you ever see a blocked state on /api/health, try QUOTALENS_IMPERSONATE=safari (default chrome).

Every poll reads your organisation's usage. The spend-limit endpoint is read once at start, and again only on a poll whose usage payload lacks the spend figures. Once a day it also reads /api/bootstrap for your plan.

Once a day it asks pypi.org for the latest version (User-Agent: quotalens/<version>, nothing else). Turn it off in Settings or with QUOTALENS_NO_UPDATE_CHECK=1. It never updates itself.

About and updates

/about (the footer's version number links to it) shows the installed version, the latest one PyPI reported and when it was last asked, the Python and install method, the data directory, database, profile and port, and the links. Check for updates asks now; it skips the daily schedule but not a one-minute limit between asks. When a newer release exists the footer adds "X available" and the settings gear gets a small neutral dot. No banner, no colour.

The page shows the command to run for how you installed it (pipx upgrade quotalens, uv tool upgrade quotalens or pip install -U quotalens); QuotaLens never runs it. A failed check (offline, PyPI down) is recorded, shown as "Couldn't reach PyPI at 14:05", and retried at the next daily slot; it is never an alert. Only plain release numbers are compared (2.0.1 over 2.0.0); pre-releases are ignored.

GET /api/version returns {current, latest, checked_ts, error, update_available, upgrade_command} from what was last stored, and POST /api/version/check asks now. /api/health also carries latest_version and update_checked_ts.

Security note

The session cookie is equivalent to your claude.ai password. QuotaLens stores it only in the OS keychain (via keyring), never in a file, the database, or a log line, and redacts it from error output. The server binds loopback only. Treat quotalens probe output as sensitive and redact it before sharing.

Storage, and what gets pruned

Two things accumulate, and only one of them is pruned.

All the figures below were measured on a real database on 2026-09-06: four days of one-minute polling, 5,807 usage samples, 1,697 overage samples and 16,066 readings in a 26.3 MB file. Yours will differ — the payload size is the endpoint's, and the row counts are your poll interval's.

The readings are the product and are never pruned. One row per window per poll, about 100 bytes each. Measured: 3,949 rows a day, 0.39 MB a day, about 144 MB a year. If you want less, poll less often.

The raw payloads are debugging material and are bounded. Every response is stored verbatim so that when the endpoint shape changes there is a record of it. Measured: a usage payload averages 2,028 bytes (range 1,897–2,073 over 5,807 samples), so at a minute a poll the usage half of the sample table grows 2.9 MB a day, about 1.05 GB a year if nothing prunes it. Something does:

quotalens prune --dry-run    # what it would remove
quotalens prune --keep 50000 # or set QUOTALENS_SAMPLE_KEEP

How long data is kept

quotalens config set retention 3months   # 1week, 1month, 3months, 6months, 1year
quotalens prune --dry-run                # what that would delete, before it does

There is no "keep forever". The period governs the detail tables — quota, overage and the raw sample cap. session_window and event are kept for two years regardless, because they are what the weekly budget table and the typical-session baseline read, and a year of them is under 2,000 rows. Choosing "1 week" will not shorten your history list.

Two things worth knowing:

  • The first run after upgrading deletes nothing. A database that predates this setting is written as 1year, the longest option, and the dashboard says so. A destructive default you did not ask for is not a default.
  • Deleting is not shrinking. SQLite keeps the freed pages, so pruning runs VACUUM and reports the real before and after. That needs up to twice the file size in free space and takes an exclusive lock, so it runs on a schedule and on a worker thread, never inside a poll.

The size shown against each option in settings is measured from your own database — your poll interval, your window count — not a figure shipped with the tool. Under two days of history it shows an em dash, because it is not knowable yet.

The default keeps the newest 20,000 samples, roughly 39 MB, plus the first sample of every distinct payload shape, forever — that set is the endpoint-drift record and pruning it would defeat the point of keeping payloads at all. The poller prunes on the same rule every six hours, so the default applies whether or not you ever run the command.

How many days 20,000 buys depends on what else is being sampled. Usage alone at a minute a poll is 14 days. The database measured above also stores an overage sample roughly every three and a half minutes — 419 a day against usage's 1,427 — and those count against the same cap, so it is about 11 days there. The panel under Data retention estimates from your own database rather than from either number.

Rows another collector wrote

If a second instance ever pointed at this database, its samples are in here too, and the history shows session windows that were never yours. That cause is fixed: a scratch --data-dir now implies a scratch database. It cannot fix databases that already have the rows.

quotalens forget                       # every session window, with its id
quotalens forget <id> [<id>] --dry-run # what removing them would take
quotalens forget <id> [<id>]           # take it, then rebuild the history

The listing marks windows where only minutes of a five-hour span were ever observed, which is what a collector that ran for two minutes leaves behind. It is a reason to look, not a verdict: a window where you genuinely only had the collector up for two minutes looks identical, and only you know which it was.

Removal is by expiry, not by time range. Two collectors writing to one database interleave their samples second by second, so deleting a time range takes real readings with it. The five-hour expiry is what separates one collector's window from another's, and it is the same key the history is built from. Stop the server first, so the startup rebuild runs on what is left:

quotalens stop && quotalens forget <id> && quotalens start

What this doesn't do

Each of these is a decision, and in most cases something else already does it better. Pointing at the better tool is a feature.

  • Per-project attribution. Use the vendor's own usage command — claude /usage inside Claude Code — for attribution to skills, subagents, plugins, MCP servers and scheduled tasks. Quota is pooled across claude.ai, Claude Code and Claude Desktop, so local logs can only ever show correlation with a number they cannot see.
  • Anything but loopback. There is no --host. The dashboard is account data with no authentication. If you want it elsewhere, put it behind a proxy you already trust; if enough people ask, the answer will be a token, not a flag.
  • A container image. pipx or uvx only, for now. Docker needs a credential path that is not the OS keychain, which forks the security story, and that deserves its own decision rather than a Dockerfile.
  • Reading Claude Code's OAuth credentials. The pasted cookie stays the only auth path in this release.
  • Any provider but Claude, API-key cost tracking, a menu bar app, a proxy, or a hosted service.

Linux servers: not yet

QuotaLens keeps your cookie in the OS keyring and has no file-based credential store. On a Linux box with no desktop session there is usually no D-Bus session and no keyring daemon, and then python-keyring has no backend at all:

$ quotalens auth
cannot use the keyring: this system has no usable keyring, so the session
cookie cannot be stored or read. On a Linux server that normally means there is
no D-Bus session and no keyring daemon; QuotaLens has no file-based credential
store, so a headless Linux box is not supported yet...

That check runs before you are asked for a cookie, so you find out in a second rather than after pasting one and waiting for a network round trip. A desktop Linux session with gnome-keyring or kwallet unlocked works normally, and so does the systemd user unit under that session.

Supporting a real server means a credential path that is not the OS keyring, and that forks the security story, so it is a deliberate post-1.0 decision rather than something to bolt on. It is the first issue on the list.

Platforms

macOS, Linux and Windows are in the CI matrix on Python 3.11 and 3.13, eight jobs (three platforms × two versions, plus lint and render), green on every push. Each one builds the wheel, installs that rather than the source tree, runs the whole suite against it, and then runs a smoke test that starts a real server against a fake upstream, polls it, reads the series back through the API, and drives start, status, logs and stop on the pid file. The Windows job additionally registers the logon task for real, queries it, and removes it, so the task definition is checked by Task Scheduler rather than by me.

Be precise about what that proves. It proves the wheel installs and the app runs on all three. It does not prove the credential path on any of them: the smoke test injects an in-memory store, so Windows Credential Manager and the macOS Keychain are exercised by hand, not by CI. Two known Windows limits: stop terminates the server rather than signalling it, because Windows has no SIGTERM; and log rotation can fail to roll the 2 MB file while the server holds it open, in which case it keeps appending rather than losing lines.

The Terms, stated plainly

Anthropic's Consumer Terms, section 3 ("Use of our Services"), prohibit:

  1. Except when you are accessing our Services via an Anthropic API Key or where we otherwise explicitly permit it, to access the Services through automated or non-human means, whether through a bot, script, or otherwise.

QuotaLens is automated access, and a subscription session cookie is not an API Key. There is no carve-out in the Terms for reading your own usage. I looked.

Two facts that bear on the risk, neither of which changes the clause:

  • I know of no case of Anthropic acting against a read-only usage monitor. The one confirmed enforcement in this space was against third-party harnesses that spent subscription quota by running completions, and those were reinstated in May 2026 with metered credits.
  • QuotaLens never sends a prompt and never spends a token. It reads two endpoints your own browser reads, at most once a minute.

You are the one accepting that risk, not me. Decide with the clause in front of you.

Two more clauses matter if you run more than one profile: section 2 of the same Terms (your credentials are yours alone) and the Usage Policy's rule against using multiple accounts to circumvent product restrictions. See Two accounts.

Unofficial, and not affiliated with or endorsed by Anthropic. It uses undocumented endpoints that may change without notice, and it only observes: it cannot raise, extend or bypass a limit.

License

MIT.

Metadata

Release files for quotalens 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for quotalens 2.0.0
File Size Uploaded
quotalens-2.0.0.tar.gz 558.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for quotalens 2.0.0
File Interpreter ABI Platform
quotalens-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 821.6 kB

Release files / quotalens-2.0.0.tar.gz

Download URL quotalens-2.0.0.tar.gz
Size 558.2 kB
Tags Source
SHA-256 checksum
How to use checksums
17c6371d9d4d17882c7ddac1088e356524ead0d8514bd1abf288ffb6732d0f2b
BLAKE2b-256 checksum
How to use checksums
659e9d278feed31157021949a1311b5dd5ec9a95312000c633c9bc81fbeb37b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / quotalens-2.0.0-py3-none-any.whl

Download URL quotalens-2.0.0-py3-none-any.whl
Size 263.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6ee906200a5d270bc0867266f6b83fec6efa847bea75d0ef44e9b1c01bb32f60
BLAKE2b-256 checksum
How to use checksums
f364ce9cb18f5b4b328ab67256e97743a039da1f1e70209831a687daef89e8fb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page