quartermaster · Documentation

Documentation

Every flag, how the output is read, the --json contract and where each number comes from. If anything here does not match what the program does, the program is right and this page is the bug: say so.

01 Getting started

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
It updates only through the manager that installed it. apt with 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.
02 Usage

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.

FlagWhat it does
qmQuota and usage for every profile. No network and no credential: the quota is the one Claude Code already left in .claude.json.
--refrescarAlso asks the endpoint for the number. Needs a live token.
--jsonThe same information as JSON, for scripts and statuslines. It is a contract: see below.
--breveOne 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=NThe window for local usage. 7 by default.
--umbral=NExits with code 3 if any bar is over N %.
--esperarDoes not return until quota drops below the threshold (80 by default). For chaining: qm --esperar && codex …
--cuenta=NOMBREWith --esperar: watch only that account.
--solo=a,bShow only those accounts.
--ocultar=a,bShow every account but those.
--sin-codexDo not look at the Codex account.
--redactadoWith --json: strips e-mails and home paths. For committing an output.
--calentarRefreshes 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,bWith --calentar: only those. Every account skipped is one request fewer to an endpoint that is not ours.
--calentar-codexThe same, but only Codex and without touching the keychain.
--diagnosticoEverything 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, --versionThe version, and nothing else.
--red / --sin-redForce or forbid the network probe. --offline is a synonym for --sin-red.
-h, --helpThe same list, in the terminal.
03 The output

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%
MarkWhat 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.
The bar you see is not always the one that stops you. Inside 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.
04 The rate

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.jsonl and is indexed by the quota's medidoEn, not by when qm looked: reading the same cache ten times leaves one sample.
05 The contract

--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

FieldWhat it is
generadoISO 8601. When qm ran — not when the quota was measured.
plataformalinux, darwin or win32.
ventanaDiasThe window for local usage. Whatever --dias set.
perfilesOne 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

FieldWhat it is
productoclaude, codex or whichever provider opencode stores.
perfil / directorioThe profile's name and where it was read from. With --redactado the home path comes out as ~.
cuenta / planThe e-mail and the plan. With --redactado the e-mail comes out masked.
credencialA sentence, in Spanish: live and how long it has left, or expired and which command fixes it.
cuota.estadook, or the reason there is no number.
cuota.origenendpoint or cache: whether the number was asked for or read off the disk.
cuota.medidoEnWhen the quota was measured. This is the date that matters.
cuota.edadSegundosHow old that number is. Past 6 h the surface marks it with ~.
cuota.ventanasEvery bar, including the ones with no key of their own at the top level.
cuota.mostrarThe ones to draw. A renderer that filters on its own is reimplementing the rule.
cuota.frenaThe bar that will stop you first. It is the one that decides the color and the warning.
cuota.sesion / semanalThe two that answer the two questions. They can be the same one.
proyeccionThe rate, or the reason there is no rate yet.
presupuestoHow 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.
localThe usage read from the transcripts, over the --dias window.
If a renderer needs to reimplement a rule, the CLI is not emitting enough. That is the rule of this repo, and the Windows tray is the proof it holds: it draws the same meters as the macOS view without a single line deciding which bar wins. It reads mostrar and frena.
06 Surfaces

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.

07 Provenance

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_DIR on 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 --dias window. 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.
08 Limits

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 (--refrescar or --calentar): to api.anthropic.com, the same host Claude Code already talks to, and to api.z.ai if 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.
09 Scripting

Exit codes

CodeWhen
0All good. Also on a machine with no profiles: the answer is "perfiles": [], not an error.
2Invalid arguments. The message says which one.
3With --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 "…"
10 Trouble

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.