Install it and run it
Needs Node ≥ 22.6. There is no build step: the CLI runs straight from TypeScript.
The shortest path is npm: one line, no repository to add, on any system. The others exist so it upgrades with the package manager you already use.
Any system — npm
$ npm install -g @legios/quartermaster
macOS — Homebrew
$ brew install legiosai/tap/quartermaster
Linux — apt
# once: the key and the repo $ sudo install -d -m 0755 /etc/apt/keyrings $ curl -fsSL https://quartermaster.legios.com.ar/apt/legios.gpg \ | sudo tee /etc/apt/keyrings/legios.asc >/dev/null $ echo "deb [signed-by=/etc/apt/keyrings/legios.asc] \ https://quartermaster.legios.com.ar/apt stable main" \ | sudo tee /etc/apt/sources.list.d/legios.list >/dev/null $ sudo apt update && sudo apt install quartermaster
From source
$ git clone https://github.com/legiosai/quartermaster $ cd quartermaster && make instalar
apt upgrade, Homebrew with brew upgrade, npm with
npm update -g. None of the three updates by itself unless you
run that command, and the program does not update itself: it has no channel
to do it with, and it is not going to get one.
Every flag
With no arguments it reads from disk and never touches the network. Everything that goes out over the network sits behind an explicit flag.
| Flag | What it does |
|---|---|
| qm | Quota and usage for every profile. No network and no credential: the quota is the one Claude Code already left in .claude.json. |
| --refrescar | Also asks the endpoint for the number. Needs a live token. |
| --json | The same information as JSON, for scripts and statuslines. It is a contract: see below. |
| --breve | One line and nothing else. It does not read transcripts, so it takes ~85 ms instead of a second. This is what goes in a statusline. |
| --watch [seg] | Redraws every N seconds. Minimum 30, 60 by default. |
| --dias=N | The window for local usage. 7 by default. |
| --umbral=N | Exits with code 3 if any bar is over N %. |
| --esperar | Does not return until quota drops below the threshold (80 by default). For chaining: qm --esperar && codex … |
| --cuenta=NOMBRE | With --esperar: watch only that account. |
| --solo=a,b | Show only those accounts. |
| --ocultar=a,b | Show every account but those. |
| --sin-codex | Do not look at the Codex account. |
| --redactado | With --json: strips e-mails and home paths. For committing an output. |
| --calentar | Refreshes the endpoint of every profile and the Codex one, stores what comes back and prints nothing. This is what the bar item runs. |
| --cuentas=a,b | With --calentar: only those. Every account skipped is one request fewer to an endpoint that is not ours. |
| --calentar-codex | The same, but only Codex and without touching the keychain. |
| --diagnostico | Everything a bug report needs, in one command and already redacted: machine, desktop, the state of the extension, the indicator and the cache, and what the journal recorded. No e-mail, no tokens, no account ids. |
| -V, --version | The version, and nothing else. |
| --red / --sin-red | Force or forbid the network probe. --offline is a synonym for --sin-red. |
| -h, --help | The same list, in the terminal. |
How to read what it shows
Every account carries two numbers, on purpose. The first is the session; the second is the bar that stops you first. They answer different questions — "can I keep going right now?" and "do I make it to the end of the week?" — and either one alone is half an answer. When the bar that stops you is the session, a single number is shown.
$ qm --breve personal 23/75%! · teams 9/42% · codex 49/59%
| Mark | What it means |
|---|---|
| 23/75% | Session at 23 %, the bar that stops you at 75 %. |
| ! | The server flagged that bar with a warning — it is not a threshold of ours, it comes in the response. |
| ~ | The cache is more than 6 hours old. The number is stale, not wrong. |
| 42% | A single number: the bar that stops you is the session one. |
cachedUsageUtilization there are bars with no key of their own
at the top level. Reading only the two famous ones —five_hour
and seven_day— showed 8 % and 59 % while the one actually
stopping you was at 75 %, flagged by the server. An error of 67 points, and
in the direction you want to believe.
Are you going to hit the wall before it resets?
It is the question neither the percentage nor the usage answers on its own: "you are at 75 %" says nothing without knowing how fast you are climbing. qm keeps a series of readings per bar and fits a line to it.
ritmo: 12.4 pts/h · 100 % en 1h58m — antes del reinicio
The line is not what matters: when it is not drawn is. With two readings, or with a window that has only just started, any extrapolation is an invented number wearing the face of a measurement. So it says this instead, which is a different thing:
ritmo: todavía no sé el ritmo: hacen falta 3 lecturas y hay 1
- Readings older than 2 hours are dropped: the rate from four hours ago does not predict the one now.
- Nothing is projected from a move smaller than 2 points, because the server sends whole numbers and a rise of one cannot be told apart from rounding.
- The series lives in
~/.cache/quartermaster/historial.jsonland is indexed by the quota'smedidoEn, not by when qm looked: reading the same cache ten times leaves one sample.
--json emits the answer, not the raw data
The four surfaces —GNOME, the macOS menu bar, the Windows tray and the browser— draw the same thing because none of them decides anything. The rule that picks which bar to show used to live copied into Python, JavaScript and Swift, and a rule in three languages is a different answer per screen the day someone edits one of them. Now the CLI emits the answer already resolved and the renderers read fields.
$ qm --json --redactado
{
"generado": "2026-09-10T23:04:07.315Z",
"plataforma": "linux",
"ventanaDias": 7,
"perfiles": [
{
"producto": "claude",
"perfil": ".claude",
"directorio": "~/.claude",
"cuenta": "***@ejemplo.com",
"plan": "claude_max",
"credencial": "vigente (4h29m)",
"cuota": {
"estado": "ok",
"origen": "endpoint",
"medidoEn": "2026-09-10T22:56:23.201Z",
"edadSegundos": 464,
"ventanas": [ … ],
"mostrar": [ … ],
"frena": { "clave": "weekly_scoped", "porcentaje": 89, … },
"sesion": { "clave": "session", "porcentaje": 36, … },
"semanal": { "clave": "weekly_scoped", "porcentaje": 89, … },
"historia": [ … ]
},
"proyeccion": { … },
"presupuesto": { "porDia": 4.2, "quedaHoy": 1.8, "barra": "weekly_scoped (Fable)", … },
"local": { … }
}
]
}
The root
| Field | What it is |
|---|---|
| generado | ISO 8601. When qm ran — not when the quota was measured. |
| plataforma | linux, darwin or win32. |
| ventanaDias | The window for local usage. Whatever --dias set. |
| perfiles | One entry per account found. It can be [] on a machine with nothing installed, and that is not an error: the exit code is still 0. |
Each profile
| Field | What it is |
|---|---|
| producto | claude, codex or whichever provider opencode stores. |
| perfil / directorio | The profile's name and where it was read from. With --redactado the home path comes out as ~. |
| cuenta / plan | The e-mail and the plan. With --redactado the e-mail comes out masked. |
| credencial | A sentence, in Spanish: live and how long it has left, or expired and which command fixes it. |
| cuota.estado | ok, or the reason there is no number. |
| cuota.origen | endpoint or cache: whether the number was asked for or read off the disk. |
| cuota.medidoEn | When the quota was measured. This is the date that matters. |
| cuota.edadSegundos | How old that number is. Past 6 h the surface marks it with ~. |
| cuota.ventanas | Every bar, including the ones with no key of their own at the top level. |
| cuota.mostrar | The ones to draw. A renderer that filters on its own is reimplementing the rule. |
| cuota.frena | The bar that will stop you first. It is the one that decides the color and the warning. |
| cuota.sesion / semanal | The two that answer the two questions. They can be the same one. |
| proyeccion | The rate, or the reason there is no rate yet. |
| presupuesto | How much can be spent per day for the long window to land exactly on its reset (porDia) and how much of that is left today (quedaHoy), or the reason it cannot be divided up. Which window gets divided is the core's call: it is the long one, never the 5 h one. |
| local | The usage read from the transcripts, over the --dias window. |
mostrar and
frena.
The six screens
All of them come out of qm --json. None of them knows what a
credential is.
Claude Code's statusline
Where the tool does what it is for: the number stops being something you have to remember to check.
GNOME
qm-indicator draws one meter per account in the top bar, and the panel lives inside an extension of its own.
The macOS menu bar
qm-barra. Live text, with the title measured rather than eyeballed.
The Windows tray
qm-tray.ps1. It draws an arc: at 16 px one digit reads and two are a smudge.
The browser
qm-web. A dashboard that redraws every 30 s.
The statusline, in the profile's settings.json
{
"statusLine": { "type": "command", "command": "qm --breve" }
}
Mind what it does not do: the cache is refreshed when that profile runs Claude Code, so a profile's statusline updates by being used. That is enough to find out you are at 75 % while you work, and not enough to watch a profile you are not using.
Where each number comes from
- The quota comes from
cachedUsageUtilization, inside each profile's.claude.json. It is already on the disk: no network and no credential are needed, which is why it is read even when the token has expired. - The profiles are found by looking at every
CLAUDE_CONFIG_DIRon the machine, not only the default directory. A work seat and a personal subscription are two profiles, and every other tool sees one. - The usage comes from the local transcripts, over the
--diaswindow. It is the floor: there is always a number, even with every token expired. - Codex does not leave its quota on the disk, so that cache is ours: it refreshes itself only when stale, except under
--breve, which has to keep taking milliseconds. - opencode contributes whichever providers it has configured, treated the same way.
What it does not do, on purpose
- It never refreshes a token. It reads credentials, it does not write them. Refreshing would mean racing Claude Code's own write and risking the user's refresh token: a broken login in exchange for a monitoring convenience. When a credential expires, it says so and carries on with the rest.
- No account, no cloud, no telemetry. Everything is read off the disk and from the user's own credential. The only things that leave the machine are the quota probes, and only when you ask for them (
--refrescaror--calentar): toapi.anthropic.com, the same host Claude Code already talks to, and toapi.z.aiif opencode shows you have used a z.ai provider. Neither one happens on the everyday path, which reads the disk. The full table is in SECURITY.md. - That probe has a floor: 60 seconds. An adaptive cadence once came down to 20 s per account —some 360 requests an hour, sustained, against an undocumented endpoint—. Getting rate-limited for reading your own quota would be a self-inflicted version of the silence this tool exists to fix.
- The transcript adapter is not optional. The endpoint is undocumented and can disappear without notice; the local floor is what guarantees there is always a number.
Exit codes
| Code | When |
|---|---|
| 0 | All good. Also on a machine with no profiles: the answer is "perfiles": [], not an error. |
| 2 | Invalid arguments. The message says which one. |
| 3 | With --umbral=N: some bar reached or passed that N %. |
# don't start a long job if we are already at 80 $ qm --umbral=80 >/dev/null || exit 0 # or wait for it to come down instead $ qm --esperar --cuenta=codex && codex exec "…"
If something is not working
It says the credential has expired
That is correct and breaks nothing: the quota is still read, because it comes off the disk. The message says which command fixes it. qm does not run it for you, on purpose.
The number has a ~ next to it
The cache is more than 6 hours old. Run qm --refrescar, or use that profile: Claude Code rewrites the cache when it runs.
An account is missing
Check with qm --json whether it shows up under perfiles. If it is not there, its CLAUDE_CONFIG_DIR is not in the environment you are running qm from.
Nothing appears in the GNOME top bar
It needs the extension and a fresh session: GNOME loads extensions at login, so once it is installed you have to log out and back in.
Something else
Open an issue. Paste the output of qm --diagnostico: it gathers everything a report needs in one command — machine, desktop, the state of the extension and the indicator, the cache and what the journal recorded — and it comes redacted, with no e-mail, no tokens and no account ids. Paste it as it is.