aiusage
One screen for every AI coding agent's rate limits. How much of each limit is spent, and when it resets — for all your Claude accounts, Codex and Antigravity at once, refreshed every minute.
Pure Python standard library: no dependencies, no services, no telemetry, no network calls except to the providers' own endpoints.
AI USAGE peak 89% next 52s 12:18:54
╭─ claude work ─────────────────────────── team ╮ ╭─ claude personal ──────────────────────── pro ╮
│ 5h ███████░░░░░░░░░░░░░░░ 31% ↻ 4h01m │ │ 5h ████████████████████░░ 89% ↻ 2h11m │
│ wk ████████░░░░░░░░░░░░░░ 35% ↻ 4d5h │ │ wk ██████████████████░░░░ 82% ↻ 1d4h │
╰───────────────────────────────────────────────╯ ╰───────────────────────────────────────────────╯
╭─ codex ───────────────────────────────── team ╮ ╭─ agy ─────────────────────────────────────────╮
│ wk ████░░░░░░░░░░░░░░░░░░ 20% ↻ 5d11h │ │ gemini ───────────────────────────────────── │
╰───────────────────────────────────────────────╯ │ 5h █░░░░░░░░░░░░░░░░░░░░░ 6% ↻ 2h44m │
│ wk █░░░░░░░░░░░░░░░░░░░░░ 6% ↻ 4d20h │
│ claude/gpt ───────────────────────────────── │
│ 5h ░░░░░░░░░░░░░░░░░░░░░░ 0% ↻ 4h58m │
│ wk ░░░░░░░░░░░░░░░░░░░░░░ 0% ↻ 6d22h │
╰───────────────────────────────────────────────╯
q quit r refresh poll 4.1s
5h is the five-hour window, wk the weekly one. Percentages are consumed, ↻ is the
time until reset. Green below 70%, yellow from 70%, red from 90%. peak in the header is the
hottest number across every agent — the one thing to look at.
Cards are packed shortest-column-first, so a two-line card never gets stretched to match a tall neighbour, and the grid folds to a single column on a narrow terminal.
Why
Every agent shows its own limits in its own place, in its own format, and only while you are inside it. When you split work across three CLIs and two Claude accounts, "which one still has room?" becomes a question you cannot answer without opening all of them.
Two design consequences worth knowing before you install:
- The agent list is not hard-coded. The dashboard scans the machine for logged-in CLIs, so a new account appears without editing anything. See Auto-discovery.
- Polling is deliberately slow. The Claude usage endpoint throttles hard, per account. The refresh windows here come from measurements, not guesses. See Polling and rate limits.
Requirements
Python 3.9+ and at least one logged-in CLI: claude,
codex, or agy (Antigravity). Anything you are not
logged into simply does not appear.
Linux and macOS. Windows is untested — the curses TUI will not work there, though --json and
--table should.
Install
Single file — no pip, no venv, no root:
curl -L https://github.com/sharaxlex/aiusage/releases/latest/download/aiusage \
-o ~/.local/bin/aiusage && chmod +x ~/.local/bin/aiusage
As a package, if you have uv or pipx:
uv tool install aiusage-tui # from PyPI
uv tool install git+https://github.com/sharaxlex/aiusage.git # or straight from the repo
The distribution is called aiusage-tui because plain aiusage was already taken on PyPI by an
unrelated project. The command and the import name are both aiusage.
From a clone, for development:
git clone https://github.com/sharaxlex/aiusage.git
cd aiusage
make install # uv tool / pipx / pip --user, whichever is available
make run # or just run it out of the clone, without installing
That is the whole setup. Run it:
aiusage
The first run has no config to read, so it scans the machine, keeps what it found in
~/.config/aiusage/config.json, and goes straight to the dashboard — every later run reads
that file and scans nothing. Log into a new profile later and aiusage init picks it up; see
Auto-discovery.
If the shell says aiusage: command not found, ~/.local/bin is not on your PATH:
export PATH="$HOME/.local/bin:$PATH"
Usage
aiusage # full-screen TUI, refreshed every minute
aiusage --once # one snapshot as cards, then exit (good under `watch`)
aiusage --json # one snapshot as JSON
aiusage --table # compact table, one line per limit window
aiusage --cols 4 # more cards per row (default 2)
aiusage --once --width 100 # fix the grid width instead of using the terminal's
aiusage --only codex --only agy
aiusage --interval 30 # seconds between screen refreshes
aiusage --force # refresh now, ignoring the freshness window
aiusage --no-cache # skip the on-disk cache (debugging; may earn a 429)
aiusage --no-refresh # never rotate OAuth tokens
aiusage init # find agents and append the new ones to the config
aiusage init --yes # no questions, for scripts and provisioning
aiusage init -n # dry run: show what would be written
aiusage init --prune # also drop agents whose files are gone
aiusage init --reset # rewrite the agent list from the scan
aiusage uninstall # remove the files the dashboard wrote
In the TUI: q or Esc to quit, r or space to refresh now (this bypasses the freshness
window, but not the backoff — see below).
When stdout is not a terminal the TUI is skipped automatically and one snapshot is printed
instead, so piping and watch work without extra flags.
Where the numbers come from
| Agent | Source | Latency |
|---|---|---|
claude |
GET api.anthropic.com/api/oauth/usage, token from ~/.claude/.credentials.json |
~0.3 s |
claude personal |
the same, from ~/.claude-personal/.credentials.json — and likewise for any ~/.claude-* profile |
~0.3 s |
codex |
GET chatgpt.com/backend-api/wham/usage, token from ~/.codex/auth.json; on failure, the last token_count from a recent rollout log (shown as from log HH:MM) |
~0.5 s |
agy |
agy --print /usage — the CLI itself prints a TSV table of the Gemini and Claude/GPT quotas |
~4–5 s |
Agents are polled in parallel, so the slow one (agy shells out to a CLI) never holds up the
fast ones, and the TUI stays responsive while it happens.
Polling and rate limits
The Claude usage endpoint throttles aggressively, and separately from everything else. Measured against claude 2.1.239, one IP, two accounts:
- The limit is per account, not per IP and not per application. At the same moment from the
same machine: work token →
429, personal token →200with fresh numbers. - It is
usagespecifically. Same token, same second:GET /api/oauth/profile→200,GET /api/oauth/usage→429. Authentication and token rotation have nothing to do with it. - The floor is roughly two minutes per account. A call 30 s after a good one is always refused. A steady 70 s cadence succeeds about every other time (6 of 11). After a refusal, access returns after roughly a minute of silence.
- Bursts are punished for a long time. A run of back-to-back requests once earned a
429lasting ~24 minutes, including six-minute pauses inside it. Retry-Afteris useless — it comes back as0. The server will not say when to return.
Claude Code itself (fetchUtilization in the binary) makes one request with a 5 s timeout and
no retries; on 429 it falls back to a seed value from ordinary API response headers or its
own ~/.claude.json → cachedUsageUtilization. It saves a new snapshot at most every 5 minutes
and treats a saved one as good for up to an hour. Five-minute staleness is normal by design.
So the dashboard works like this:
- A freshness window per agent — 10 minutes for Claude (6 requests per hour per account, comfortably inside the measured limits), 2 minutes for Codex and agy. The screen still ticks every minute to recompute the countdowns, but only genuinely stale agents hit the network.
- An on-disk cache at
~/.cache/aiusage/state.json, shared by every process, sowatch -n60 aiusage --oncedoes not hammer the API either. Data older than 90 seconds makes the card show when it was fetched:╭─ claude work ──── team · 12:41 ╮. - Exponential backoff on 429 and 5xx: 5 → 10 → 20 → 30 minutes, or longer if the server did
send a
Retry-After. The card does not go blank — it keeps showing the last known numbers plusretry in 4m. One success clears the backoff. - No retries inside a single poll, exactly like the client: a
429means back off.
Tune it with "min_interval" on an individual agent. To wipe the state entirely,
rm ~/.cache/aiusage/state.json.
OAuth tokens and privacy
The dashboard reads the credential files the CLIs already keep, and talks only to the providers' own endpoints. There is no telemetry, no analytics, no third-party host in the code — there are exactly four outbound URLs in the source (two usage endpoints, two token endpoints), all official:
api.anthropic.com/api/oauth/usage platform.claude.com/v1/oauth/token
chatgpt.com/backend-api/wham/usage auth.openai.com/oauth/token
agy is not on that list: it is a local subprocess, and whatever network access it needs is its
own business.
Claude access tokens live 8 hours; Codex has its own lifetime. When one has expired, the
dashboard refreshes it with the stored refresh token and atomically rewrites the same file the
CLI reads (~/.claude*/.credentials.json, ~/.codex/auth.json), preserving the format and
0600 permissions. That is deliberate: if the dashboard kept its own copy, the two would rotate
independently and eventually fight. Concurrent runs are guarded by a lock file.
If you would rather it never wrote to a credentials file, use aiusage --no-refresh (or
"refresh": false on an agent). Expired tokens then show as
token expired — run claude to re-authenticate instead of numbers.
Uninstall
aiusage uninstall lists everything the dashboard has written and removes it after one
question:
$ aiusage uninstall
aiusage wrote these files:
~/.config/aiusage/config.json 412 B
~/.cache/aiusage/state.json 3.1 KB
Credentials of claude and codex are not touched.
Remove them? [y/N] y
Removed 2 files.
To remove the command itself:
uv tool uninstall aiusage-tui
Add --yes to skip the question. That really is the whole footprint — no services, no logs, and
nothing outside those two files except a short-lived
~/.claude*/.credentials.json.aiusage-lock held during a token refresh, which uninstall also
sweeps up if a killed process left one behind.
Credentials are deliberately out of scope: they belong to the CLIs, and deleting them would log
you out of claude or codex. Rotated tokens stay in those files, where they belong — removing
the dashboard breaks no login.
The command cannot delete the program it is running from, so it prints the right line for the
way you installed it (uv tool uninstall aiusage-tui, pipx uninstall aiusage-tui,
rm ~/.local/bin/aiusage for the zipapp, or make uninstall from a clone).
Auto-discovery
The agent list is not in the source. The dashboard looks for the files the providers would read anyway, with no network access:
| Type | Where it looks | What proves a login |
|---|---|---|
claude |
~/.claude*, ~/.config/claude*, plus $CLAUDE_CONFIG_DIR |
a .credentials.json inside |
codex |
~/.codex*, ~/.config/codex*, plus $CODEX_HOME |
an auth.json inside |
agy |
PATH, then ~/.gemini/antigravity-cli/bin/agy, ~/.local/bin/agy, /usr/local/bin/agy, /opt/homebrew/bin/agy |
an executable file |
Card names come from the directory name: leading dot dropped, - and _ become spaces.
~/.claude → claude, ~/.claude-personal → claude personal, ~/.codex_backup →
codex backup. Names must be unique, because the name is the cache key, so a clash gets a
disambiguator (claude work and claude work (config)).
Plan and e-mail are read from the same files, but only ever printed by init, to tell one
account from another. They are not written to the config: a plan changes with the subscription,
and a name has to stay stable.
Profiles that physically cannot report usage — an API key in the credentials instead of OAuth —
are marked [-] and skipped rather than added as a permanently red card. A profile with a dead
refresh token is added, flagged token expired; logging in again is enough.
Re-running init
init is meant to be run whenever: log into a new profile, run aiusage init, and it appends
the agent. Everything already in the config is left exactly as it is — renamed cards,
enabled: false, a custom min_interval. Rows are matched by type and path, not by name, so
a renamed card is not duplicated, and a re-run with nothing new prints Nothing to change and
does not touch the file.
| Command | Effect on the config |
|---|---|
aiusage init |
append new agents only |
aiusage init --prune |
append new, remove those whose files are gone |
aiusage init --reset |
rewrite the agent list from the scan |
--prune is for deleted profiles, which otherwise sit in the config forever as a
no credentials — log in first card. It only removes rows that lost their evidence of a login;
hidden-but-alive agents are kept, as are agents of a type the dashboard does not know — it will
not judge someone else's provider.
--reset means "go back to what is visible on the machine", and everything the rewrite costs is
shown rather than applied silently:
The agent list will be rewritten from the scan:
[+] claude ~/.claude team
[+] claude personal ~/.claude-personal pro
Dropped:
[x] claude old profile ~/.claude-gone not found on this machine
[x] claude by hand ~/work/profile still works, but the scan cannot find it
[!] claude at work ~/.claude name 'claude at work', enabled: false, min_interval: 900
Note the second [x]: a profile configured by hand outside the search patterns is alive, but
the scan cannot see it, so --reset would drop it. That is why it gets its own reason, and why
[!] spells out which manual settings would be lost. interval, columns and any key this
version does not know about survive a --reset — only the agent list is rewritten.
The first run
With no config on disk, aiusage scans, saves what it found, and says so once in the footer:
saved 3 agents to ~/.config/aiusage/config.json. Installing and running is therefore the whole
setup — nothing to configure before the dashboard is useful.
Saving is what makes the next run cheap and predictable: as long as the config lists agents,
the dashboard does not stat a single extra path, and the file is there to rename a card, hide
one with enabled: false, or give one its own min_interval. Noticing a new profile and
running init is then your call — nothing rescans behind your back.
The first run refuses to save in exactly two cases. An empty scan is not written, because
recording "nothing installed" would stop every later run from looking again — you get
no agents found — run aiusage init instead. And an existing config is never touched,
whatever is in it: a file with no agents in it means someone edited it that way, and
rewriting it is init's job, not a side effect of starting the dashboard. A config directory
that cannot be written to is not an error either — the dashboard just runs off the scan and
says auto-detected — run aiusage init to save.
Configuration
The first run writes one (see above), and aiusage init rewrites it. You only need to edit it
by hand to change something the scan cannot know — a card's name, whether it is shown, its
refresh window:
{
"interval": 60,
"columns": 2,
"agents": [
{"name": "claude", "type": "claude", "config_dir": "~/.claude"},
{"name": "claude personal", "type": "claude", "config_dir": "~/.claude-personal"},
{"name": "codex", "type": "codex", "home": "~/.codex"},
{"name": "agy", "type": "agy"}
]
}
Per-agent fields: enabled: false hides the card, refresh: false forbids token rotation,
min_interval overrides the freshness window in seconds. codex takes home; agy takes bin
and timeout. A third Claude account is one more row with a different config_dir — or just log
in and re-run aiusage init.
Environment: AIUSAGE_CONFIG overrides the config path (default
~/.config/aiusage/config.json), AIUSAGE_CACHE the cache path (default
~/.cache/aiusage/state.json).
Troubleshooting
| What you see | What it means |
|---|---|
no credentials — log in first |
no .credentials.json — start that CLI and log in |
refresh rejected — run claude to log in |
the refresh token is dead, or refreshTokenExpiresAt has passed |
from log HH:MM on codex |
the API was unreachable; these numbers come from the last session log |
unparsed /usage (...) on agy |
agy changed its output format, or is not logged in — check agy --print /usage |
timeout after 45s on agy |
the CLI did not answer; raise "timeout" in the config |
rate limited · retry in 4m |
the API refused (429); the last cached numbers are shown, with a countdown to the next attempt |
team · 12:41 in a card header |
the data is cached, fetched at 12:41 (fresh data shows no time) |
saved 3 agents to ~/.config/aiusage/config.json |
the first run froze what it found; later runs read the file |
auto-detected — run aiusage init to save in the footer |
running off the scan without saving — usually a config directory that cannot be written to |
no agents found — run aiusage init |
nothing logged in was found — log into claude or codex and retry |
[-] api key mode — no usage endpoint in init |
the credentials hold an API key, not OAuth: limits are not exposed that way, so the profile is skipped |
[x] not found on this machine in init --reset/--prune |
the configured profile is gone from disk; the row can go |
[x] still works, but the scan cannot find it |
a live profile outside the search patterns: --reset would drop it, --prune would not |
Development
pyproject.toml build (hatchling), the `aiusage` entry point, zero dependencies
Makefile run / test / install / zipapp / clean
tests/ no network: providers are stubbed, discovery runs against a fake $HOME
src/aiusage/
__main__.py CLI: flags, the init subcommand, mode selection (TUI / --once / --json)
config.py defaults, ~/.config/aiusage/config.json, merging with the scan
discover.py scanning $HOME and PATH, names from paths
initcmd.py the `aiusage init` wizard: show, ask, write
uninstall.py `aiusage uninstall`: list what was written, remove it
collector.py parallel polling: what is stale, what is cached, what is backing off
model.py Report / Window — the one shape all data takes
cache.py last-good values plus backoff, shared by every process
cards.py cards: frames, responsive grid, styled segments
render.py bars, percentages, time formatting, the --table mode
tui.py the curses screen, background polling, the timer
httpx.py JSON over HTTPS on the standard library
credfile.py the credential files the CLIs keep: atomic rewrite, JWT claims
providers/
claude.py OAuth usage + token rotation
codex.py wham/usage + the rollout-log fallback
agy.py parsing `agy --print /usage`
make test # or: PYTHONPATH=src python3 -m unittest discover -s tests
make run # run from the clone without installing
make zipapp # build the single-file executable into dist/ (~80 KB)
CI runs the tests on 3.9 through 3.14 on Linux plus one macOS job, and separately builds the wheel, the sdist and the zipapp and runs each of them. The test job installs nothing at all — if it ever needs to, the "runs on a bare python3" promise is gone.
Across the matrix with uv:
for v in 3.9 3.10 3.11 3.12 3.13 3.14; do
PYTHONPATH=src uv run --no-project --python $v python -m unittest discover -s tests
done
This is a src/ layout: import aiusage does not work from the repository root, so the tests
always exercise what actually ships rather than the directory next door. The single-file build
is possible only because there are no dependencies at all.
Adding a provider
A provider is a function fetch(spec) -> Report | list[Report] that never raises: failures go
into Report.error. Register it in providers/REGISTRY and add a row to the config.
To make it discoverable too, add a scanner in discover.py (_claude / _codex / _agy are
the models) and one line to discover.LAYOUT: the spec field holding its path, the default the
provider assumes, and the file that proves a login. That single table is what init uses to tell
agents apart and what --prune uses to decide whether one is still there. Without it the
provider simply is not discovered — configured by hand, it still works.
Disclaimer
Unofficial and unaffiliated with Anthropic, OpenAI or Google. It reads local credential files and calls the providers' own endpoints; it is not built on any documented, stable API, so a provider can change a response shape and break a card at any time.
License
MIT.
Metadata
Release files for aiusage-tui 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aiusage_tui-0.1.0.tar.gz | 48.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aiusage_tui-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 98.8 kB
Release files / aiusage_tui-0.1.0.tar.gz
| Download URL | aiusage_tui-0.1.0.tar.gz |
|---|---|
| Size | 48.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ade1647fc5185e195e695769caa21ca1aa2f2844cff8679c0b37b9f79ecfadf1
|
|
BLAKE2b-256 checksum How to use checksums |
bec22d100773b34157bc98b98dc9aaa49f246b01d0845c1dd0aaed63e7446cbf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / aiusage_tui-0.1.0-py3-none-any.whl
| Download URL | aiusage_tui-0.1.0-py3-none-any.whl |
|---|---|
| Size | 50.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7442910d36b7002af34d38df63beeb2180869dcbc1a6bce463f50ddbb9bdf380
|
|
BLAKE2b-256 checksum How to use checksums |
923d369e87a491340026a49917003e5d36726469fcdf0d3140d75a6f0faf2609
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|