Records your Claude Code plan usage for the Quota Pill macOS app

A floating macOS pill that shows how much of your GitHub Copilot, Codex, Claude Code, and Antigravity quotas you have used.
CP 47 CX 6 CC 69 AG 0
Each number is the fullest window for that harness, in %. – means the source is currently unavailable, and ! means an error occurred. Numbers are blue, turn orange at 80%, and red at 95%. When a threshold is crossed, macOS shows one notification per threshold in each reset cycle.
The pill stays visible over full-screen apps. Hover over it to expand the details for each window, and click a section to open that service's usage page (or the Antigravity app). You can drag it, and its position is saved. The ↻ in the footer refreshes everything, while the ↻ shown when hovering over a section refreshes only that harness and bypasses the cache. Right-click offers "Refresh now" and "Quit".
You only need the harnesses you actually use. The others show –.
xcode-select --install), for Swift 6.2. Xcode 26 needs macOS 15.6 or later.git clone https://github.com/faelplg/quota-pill.git
cd quota-pill
bun install
bun run install-pill
install-pill builds the app and registers a LaunchAgent (~/Library/LaunchAgents/org.quota-pill.agent.plist) that starts it at login and restarts it after a crash, but respects "Quit". Installation and uninstallation also remove older Quota Pill agents pointing to the same checkout.
The pill runs from the checkout, so keep the folder where it is. If you move it, run bun run install-pill again. To update, pull and reinstall:
git pull
bun install
bun run install-pill
To remove it:
bun run uninstall-pill
Claude Code reports your plan usage only to code running inside it, so the Claude Code numbers come from a small plugin in this repository. Install it once:
claude plugin marketplace add faelplg/quota-pill
claude plugin install quota-pill@quota-pill
Inside a session, /plugin marketplace add faelplg/quota-pill and /plugin install quota-pill@quota-pill do the same. Claude Code keeps a copy of the installed version, so update it when the plugin changes:
claude plugin marketplace update quota-pill
claude plugin update quota-pill@quota-pill
If your Claude Code settings are managed declaratively (for example with Nix), load the plugin straight from your checkout instead. It then updates with git pull:
{
"env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/quota-pill/plugins/quota-pill" }
}
The plugin is a mod with one hook. After each turn, and whenever a plan window moves, it writes the windows Claude Code measured to ~/Library/Caches/quota-pill/claude-rate-limits.json. It needs no credentials and makes no network requests. Tested end to end with Claude Code 2.1.286, and CI checks it against 2.1.296. To remove it, run claude plugin uninstall quota-pill@quota-pill.
Only the Claude Code source is a documented interface. The others are not public APIs and may change without notice. Each adapter fails independently and shows what happened in the pill.
| Harness | Source | Setup |
|---|---|---|
| Copilot | gh api /copilot_internal/user, plus the billing usage summary for extra spend | Install the GitHub CLI and run gh auth login. When you allow spending past your plan, an "Extra" window shows how much of that budget you used. To see it in dollars, run gh auth refresh -h github.com -s user; without it, the window shows units. |
| Codex | Latest rate_limits event in ~/.codex/sessions (or $CODEX_HOME) | None. Updates only when you use Codex. Windows whose reset has passed are shown as zero. |
| Claude Code | Plan windows recorded by the Claude Code plugin | Install the plugin. Updates only when you use Claude Code, on a Pro or Max plan. Windows whose reset has passed are shown as zero. |
| Antigravity | GetUserStatus in the app's local language server | None. Works only while Antigravity is open. |
Quota Pill has no server and no telemetry, and it never reads or stores credentials itself.
gh and its own login, and to Antigravity's language server on 127.0.0.1. Codex and Claude Code are read from local files.~/Library/Caches/quota-pill/ (or $XDG_CACHE_HOME/quota-pill/). The Claude Code snapshot always stays in ~/Library/Caches/quota-pill/, because Claude Code and the pill's LaunchAgent do not share environment variables.claude plugin list shows quota-pill@quota-pill as enabled (or that CLAUDE_CODE_PLUGIN_DIRS points to the plugin), then send a message in Claude Code. On an API key instead of a Pro or Max plan there are no plan windows to show. Organizations that allow only managed mods block the plugin.~/.bun/bin, Homebrew, /usr/local/bin, and Nix profiles. Install it in one of those places.src/cli.ts by walking up from its executable. If you run a copy outside the checkout, set QUOTA_PILL_ROOT to the project root.bun start in the checkout. It prints the same JSON the pill reads.bun start # JSON report without alerts (accepts --only <harness> and --force)
bun run pill # builds and runs the pill once in the foreground
bun test
bun run typecheck
src/adapters/ contains one file per harness, and all adapters return the same shape (HarnessReport in src/types.ts). src/cli.ts combines everything into JSON and triggers alerts, while src/alerts.ts stores notification state in the cache folder.
The Claude Code plugin lives in plugins/quota-pill/, and the repository root is its marketplace. Its tests run with Claude Code's own test kit:
claude plugin validate .
cd plugins/quota-pill && claude plugin test
To try a change in a session without installing it, run claude --plugin-dir plugins/quota-pill.
The pill is a SwiftUI app in app/. It runs bun src/cli.ts every two minutes and when the Mac wakes. After changing Swift code, run bun run install-pill again to rebuild and restart it.
The locally generated LaunchAgent contains absolute paths to your checkout. Do not commit it, local caches, credentials, or build artifacts.
Issues and pull requests are welcome. Run bun test and bun run typecheck before opening a PR, and claude plugin test if you changed the plugin. CI also builds the pill on macOS 15 and macOS 26. A new harness is a new file in src/adapters/ plus its tests in test/.
Quota Pill is an independent project and is not affiliated with or endorsed by GitHub, OpenAI, Anthropic, or Google. Product names belong to their owners.
hooks/register.ts 38 lines1import type { Hook, On, SessionRateLimit } from "claude-code";
2import { snapshotPath, toSnapshot } from "./snapshot.js";
3
4type Api = Parameters<Hook<"session.measure">>[0];
5
6/** Records the plan windows Claude Code measures, for the pill to read. */
7export function register(on: On) {
8 on("session.measure", async ($, e, next) => {
9 // An empty list is recorded only when it is news, such as leaving a Pro or
10 // Max plan, so a new session's first measurement never clears the pill.
11 if (e.rateLimits.length > 0 || e.changed.includes("rateLimits")) {
12 try {
13 await record($, e.rateLimits);
14 } catch {
15 // Never disturb the session. The pill keeps showing the last snapshot.
16 }
17 }
18 return next(e);
19 });
20}
21
22async function record($: Api, rateLimits: readonly SessionRateLimit[]) {
23 const home = await $.env.get("HOME");
24 if (!home) return;
25 const path = snapshotPath(home);
26 const text = JSON.stringify(toSnapshot(rateLimits, new Date(await $.clock.now())));
27
28 // $.fs.write replaces content in place, so write beside the file and move it
29 // over, keeping the collector from reading a half-written snapshot.
30 const temporary = `${path}.${crypto.randomUUID()}.tmp`;
31 await $.fs.write(temporary, text);
32 const moved = await $.process.run(["/bin/mv", "-f", temporary, path]);
33 if (moved.exitCode !== 0) {
34 await $.process.run(["/bin/rm", "-f", temporary]);
35 await $.fs.write(path, text);
36 }
37}
38hooks/snapshot.ts 37 lines1/**
2 * The file the Claude Code plugin writes and the collector reads. Kept free of
3 * Bun and Claude Code APIs so both sides can import it.
4 */
5
6/** One plan window as Claude Code reports it to mods. */
7export interface RateLimit {
8 /** `five_hour`, `seven_day`, or a gateway's `spend_limit`. */
9 kind: string;
10 /** 0 to 100, or past 100 on an exceeded spend limit. */
11 percentUsed: number;
12 /** ISO 8601. */
13 resetsAt?: string;
14}
15
16export interface RateLimitSnapshot {
17 /** ISO 8601 time Claude Code measured these windows. */
18 capturedAt: string;
19 rateLimits: RateLimit[];
20}
21
22/**
23 * Fixed under the home folder on purpose. Claude Code and the pill's
24 * LaunchAgent run with different environments, so a variable such as
25 * XDG_CACHE_HOME could point each side at a different file.
26 */
27export function snapshotPath(home: string): string {
28 return `${home}/Library/Caches/quota-pill/claude-rate-limits.json`;
29}
30
31export function toSnapshot(rateLimits: readonly RateLimit[], capturedAt: Date): RateLimitSnapshot {
32 return {
33 capturedAt: capturedAt.toISOString(),
34 rateLimits: rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ kind, percentUsed, resetsAt })),
35 };
36}
37