SLOPSHOPPER

quota-pill

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

newprocess
v0.1.0MITupdated 2026-10-15faelplg/quota-pill/plugins/quota-pill
A shopper browsing a rack in a slop shop
README

Quota Pill

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

Requirements

  • macOS 15 or later. On macOS 26 the pill uses Liquid Glass; earlier versions get a blurred background.
  • Bun.
  • Xcode 26 or its Command Line Tools (xcode-select --install), for Swift 6.2. Xcode 26 needs macOS 15.6 or later.

Installation

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 plugin

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.

Sources

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.

HarnessSourceSetup
Copilotgh api /copilot_internal/user, plus the billing usage summary for extra spendInstall 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.
CodexLatest 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 CodePlan windows recorded by the Claude Code pluginInstall the plugin. Updates only when you use Claude Code, on a Pro or Max plan. Windows whose reset has passed are shown as zero.
AntigravityGetUserStatus in the app's local language serverNone. Works only while Antigravity is open.

Privacy

Quota Pill has no server and no telemetry, and it never reads or stores credentials itself.

  • The only network requests go to GitHub, through 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.
  • Alert state and other caches are kept in ~/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.

Troubleshooting

  • Claude Code shows "No Claude Code usage recorded yet". Check that 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 not found". The pill looks for Bun in ~/.bun/bin, Homebrew, /usr/local/bin, and Nix profiles. Install it in one of those places.
  • "Cannot locate the collector". The app finds 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.
  • To see raw data, run bun start in the checkout. It prints the same JSON the pill reads.

Development

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.

Contributing

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

Disclaimer

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.

License

MIT

Source 2 files
hooks/register.ts 38 lines
1import 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}
38
hooks/snapshot.ts 37 lines
1/**
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