oh-my-subscriptions

Track and safely route Claude Code, Codex, Gemini, and Kiro subscription quotas across multiple accounts

Packages

Package details

extension

Install oh-my-subscriptions from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:oh-my-subscriptions
Package
oh-my-subscriptions
Version
0.33.1
Published
Sep 18, 2026
Downloads
947/mo · 947/wk
Author
2060won
License
MIT
Types
extension
Size
168.3 KB
Dependencies
0 dependencies · 3 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions/oms.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

oh-my-subscriptions

One terminal for Claude Code, OpenAI Codex, Google Gemini / Antigravity, and Kiro subscriptions.

Track AI coding subscription quotas, manage multiple OAuth accounts, choose models and reasoning effort, route Pi to a safe account, and block accidental paid overage.

npm CI release license

Quick start · Pi integration · Providers · Safety · Commands · Contributing

What is oh-my-subscriptions?

oh-my-subscriptions (OMS) is an open-source AI subscription manager and quota-aware router for developers who use more than one coding agent plan. It answers three practical questions before work starts:

  1. Which Claude, Codex, Gemini, or Kiro account resets next?
  2. Which account still has safe subscription quota?
  3. Could this request spill into paid credits or metered overage?
$ oms status
ACCOUNT     VENDOR  PLAN  USED                              RESETS  ON PACE FOR   AS OF  BILLS?
codex-main  codex   team  5h 18%  7d 11%  (142/5000 cr)     3h 2m   full in 41m   0m     at risk
codex-alt   codex   pro   7d 60%                            4d 15h  71% by reset  0m     no
kiro-1      kiro    -     30d >=2% (21/1000 cr, cli only)   16d 7h  -             0m     no

Why use it?

  • One quota dashboard: see usage, reset time, freshness, pace, and billing risk.
  • Multiple accounts: isolate work, personal, and client subscriptions without mixing sessions.
  • Safe routing: select the next usable account by actual reset time and fail closed.
  • Paid-overage protection: block an account before subscription usage becomes a charge.
  • Shared OAuth core: connect Claude, Codex, and Google credentials once for supported hosts.
  • Pi TUI: choose an account, model, and reasoning effort without editing JSON.
  • Local-first: the standalone CLI is one Python standard-library file; the core binds to loopback.

OMS does not create free quota, bypass provider limits, pool strangers' accounts, or resell access. It organizes subscriptions you are authorized to use and keeps routing decisions explicit.

Quick start

Standalone quota CLI

One file, Python 3 standard library only, no dependencies:

curl -fsSL https://raw.githubusercontent.com/goosull/oh-my-subscriptions/main/bin/oms \
  -o ~/.local/bin/oms && chmod +x ~/.local/bin/oms

Then add your accounts. oms login installs the provider's CLI if it is missing, signs you in, names the account and spends one throwaway turn so it reports usage right away:

oms login codex          # repeat for a second account on the same provider
oms login kiro my-kiro
oms status

Claude Code plugin

The same repository is also a Claude Code plugin, which adds slash commands and the status line. It is not required to use oms, and oms does not need Claude Code.

claude plugin marketplace add goosull/oh-my-subscriptions
claude plugin install oms@oh-my-subscriptions
oms install    # wires usage recording into ~/.claude/settings.json

oms install sets statusLine in ~/.claude/settings.json, chaining whatever status line you already had. It is also how a Claude account gets its usage numbers at all — that figure comes from Claude Code and exists nowhere else.

With the status line installed, every account stays visible while you work — the one you're in with both its windows, the rest with their hottest one:

Opus 5 high  ctx ▬▬▬▬▬▬──── 61% left of 1M
claude-main   ▬▬▬▬────  44% ▬▬──────  27%
claude-alt    ────────   0% ▬───────   8%
codex-main !                ▬▬▬▬▬▬▬▬  97%
kiro-1        kiro reports no usage

One account per row, one column per window ordered shortest first — so the column a meter sits in says which window it is, and the labels that used to repeat against every account are gone. An account with no short window leaves that column empty rather than sliding into it. The account you are in is the bright one, ! marks an account that would bill you, and an account with nothing to meter keeps its row and says why instead of vanishing from the list.

Every row starts with its name rather than with indentation, because Claude Code strips leading whitespace from a status line and a leading marker column would collapse on exactly the rows that lack one.

The first line is the session itself — model, reasoning effort, and context left — so a model swap or a mid-session /effort change shows up immediately. Every bar fills in the direction of its number: account meters fill as quota is spent, the context meter drains as context is used.

Accounts appear in the order oms auto would pick them — the one you're in first, then its own vendor's alternatives, so the next account to switch to is always the next chip. Every window is shown and labelled, so a number is never ambiguous about which quota it describes. The status line cannot scroll, so rather than overrun and get truncated it wraps at your terminal width (COLUMNS, which Claude Code sets for the script) and stops after three rows, collapsing any remainder to +N more so a long account list can never swallow the screen.

Green under 60%, amber above. Red, with a leading !, means money and only money — a plan with no credit attached can sit at 99% without turning red, because it stops rather than charges. The bar is drawn with a filled and a lighter , so it still reads on a terminal with no colour.

Installed as a Claude Code plugin, two skills keep it out of the terminal: /oms:status answers "which account should I use?" mid-session, /oms:priority shows or changes the pick order, /oms:config sets an account's model or effort, /oms:rename renames one, and /oms:handoff writes the brief that carries your work to another account.

Inside Pi

Requires Pi 0.85.1+, Node.js 22+, Python 3, and the OMS CLI from the install section above. Install the stable Pi package and verified provider core:

pi install npm:oh-my-subscriptions
oms core install

The core is a loopback-only sidecar built on CLIProxyAPI v7.3.4. It owns OAuth, token refresh, credential storage, provider protocols and exact-account execution. OMS owns account names, priority, quota/billing guards and active/next selection. Pi, Claude Code and Codex are thin clients of the same core; they do not implement provider login independently.

Add accounts through the core. Supplying the OMS account name makes login, binding, quota refresh and guard sync one operation:

oms core login codex codex-personal
oms core login claude claude-work
oms core login google google-work

google uses the core's supported Antigravity/Google OAuth path. After browser authorization, the credential is automatically mapped and synced; no separate sync command is required. Existing direct oms login profiles remain readable for migration.

Install or change accounts, then restart Pi (or /reload). Enable global automatic selection with:

oms set pi_auto=yes

Before every turn OMS picks the first fresh, unblocked mapped account, pins its exact core credential, and refuses hidden fallback. Runtime 429/cooldown state overrides stale quota summaries. The exact selection persists across core restarts.

Pi's normal footer stays in place. The right side below provider/model shows:

MCP … ponytail …        Current Account: codex-personal (44% used 5d 20h)
                         Next Account: google-work (0% used 6d 23h)

BLOCKED, STALE, and unknown states are explicit. /oms shows the full table. All settings live in ~/.oms/config.json (or $OMS_HOME/config.json) and apply across working folders and supported host adapters. Global pi_auto controls automatic routing. usage_refresh_seconds controls refresh cadence. The optional usage_display can be widget, status, or off; usage_widget is its legacy yes/no compatibility alias.

Open the interactive account/model/effort picker inside Pi:

/oms

Navigate with arrows, type to search, press Enter to select, and Escape to cancel. The same picker is available with Ctrl+Shift+O. It only saves the account's global default to ~/.oms/config.json; it does not change the provider/model of the current Pi session. The CLI remains available for scripts:

oms models google-work
oms set google-work model=gemini-3.7-flash-high effort=high
oms config

The same account configuration is then used by Pi selection and terminal launchers. Switching the current Pi session is a separate action:

/oms use              # opens the available-account switcher
/oms use google-work  # direct session switch
/oms use auto         # resume automatic session selection

Or launch a new Pi session with that account's saved model and effort:

oms pi google-work
oms pi gemini       # alias when exactly one Gemini account is mapped

Core lifecycle and host commands:

oms core status
oms core start
oms core stop
oms core run codex codex-personal
oms core run claude claude-work

The core binary is downloaded from the matching GitHub release, verified by SHA-256, and stored privately under ~/.oms. Provider credentials never pass through the Python CLI or Pi extension.

How quota and safety signals work

ON PACE FOR projects where the current quota window lands if the recent burn rate continues. Under 100% means subscription quota is likely to expire unused; over 100% means the account is on course to run out early. OMS waits for two readings far enough apart before projecting—a single sample is not a trend.

AS OF reports reading age. A stale reading is treated as unknown for automatic routing, not as a conveniently low number. Claude readings naturally age between Claude Code sessions because its supported usage signal arrives through the status-line input.

BILLS? distinguishes a hard plan ceiling from paid overflow: no means the provider stops, at risk means crossing the ceiling can charge, and BLOCKED means OMS refuses to launch that account unless explicitly forced.

Releases and package publishing

.github/workflows/npm-release.yml publishes on GitHub Release published, or manual dispatch with an existing v<version> tag. It validates and tests that exact checkout before publishing with provenance. Prerelease versions use npm's beta tag; stable versions use latest. Tag and package.json version must match.

The npm package is bootstrapped and its Trusted Publisher is configured as:

  • GitHub owner: goosull
  • Repository: oh-my-subscriptions
  • Workflow filename: npm-release.yml
  • Environment: leave empty
  • Allowed actions: enable direct npm publish (not only staged publishing)

The Actions workflow uses OIDC, not an NPM_TOKEN secret. Create a new matching version/tag/release or manually dispatch an unpublished version. Bootstrap version 0.32.0-beta.2 was published locally; v0.32.0-beta.1 is GitHub-only.

Working on it

git config core.hooksPath .githooks   # once: refuses a commit the checks would fail
./bin/oms --selftest                  # the tool, including that the guard stops a launch
python3 .github/check-docs.py         # that nothing documents a command the CLI lacks
python3 .github/check-wired.py        # that nothing was built and left unconnected
cd agent && bun run typecheck         # the agent, actually typechecked
cd agent && bun test                  # the loop
cd core && go test -race ./...        # shared OAuth/account core

The same checks run in CI. The hook exists because they found a red selftest after it was already pushed, which is a broken branch and a second commit apologising for the first.

Providers

isolation where the numbers come from
claude CLAUDE_CONFIG_DIR=<dir> rate_limits in the statusLine stdin JSON
codex CODEX_HOME=<dir> account/rateLimits/read over the JSON-RPC its app-server speaks
kiro KIRO_HOME=<dir> — settings and sessions only credits metered per turn in its session files — a floor

Kiro is a partial fit and oms says so rather than pretending. KIRO_HOME moves its settings and sessions, but not its login: the credential is a single machine-wide keychain entry, and a brand new KIRO_HOME still reports the account already signed in. So Kiro holds one login at a time — registering a second Kiro account warns that logging into it signs the other out.

For codex that answers for an account you have not touched in days, and carries the absolute credit allowance too, so size sets itself. Claude has no equivalent, so its figure is only as fresh as its last session — which is what AS OF is for.

Kiro has no usage command — user is only login/logout/whoami, profile is for IdC accounts, and the real balance lives server-side behind kiro-cli dashboard, which just opens a browser. What it does leave on disk is the cost of each turn, in credits, so oms sums those for the current billing period. That needs the plan's allowance and reset day, which only you know:

oms set kiro size=1000 resets_on=1

The result is reported as >=, because it is a floor rather than a balance: it counts only CLI turns made through this profile, so Kiro IDE and web spend is invisible to it, and it goes stale if sessions are pruned. Clear size to stop tracking it at all.

A provider you have not installed is still a provider. oms login kiro offers to install the CLI first and then logs you in:

$ oms login kiro kiro-1
installing kiro-cli
$ brew install --cask kiro-cli

It picks the first install route whose own tool is on your machine — Homebrew before the curl script, for instance — and echoes the command before running it. You named the provider, so installing its CLI is the prerequisite of what you asked for rather than a surprise; --no-install refuses instead.

Naming the account up front logs straight into it, adopting one you already registered. Leave the name off and oms asks once the login succeeds, so a failed login never leaves a named half-account behind. oms remove <name> unregisters one, and --purge also deletes the profile directory, but only ever one oms created itself.

All three environment variables are documented, supported configuration. oms never calls a vendor API, never reads or writes a credential, and never handles a token. It reads a documented extension point and files the vendors' own CLIs wrote on your disk, and it launches the vendors' own CLIs to do the work.

Non-default claude profiles symlink skills/ plugins/ agents/ commands/ settings.json CLAUDE.md output-styles back to ~/.claude, so a second account isn't a bare install.

The catch

Usage is only as fresh as the last time you actually used that account — that's what the AS OF column is for. There is no supported way to poll a plan you aren't using, and oms does not try to invent one.

Switching also happens at launch, not mid-session: a running CLI holds its own session. oms status tells you when it's worth restarting somewhere else.

Which account gets used first

oms auto picks by next actual reset first, then smallest pool. The account whose current quota period ends soonest goes first, regardless of whether its window is 5 hours, 7 days, or 30 days. If reset times match, the smaller pool goes first; if those also match, the lower-used account goes first.

$ oms priority
priority: automatic - next reset first, then smallest pool

  codex  1. codex-main   resets in 3h 2m, 18% used, pool 5000   can bill
  codex  2. codex-alt    resets in 4d 15h, 60% used             cannot bill

The order and what it costs you sit on the same line on purpose: reading one off oms priority and the other off oms status is how a reserve gets burned while an expiring window evaporates.

Pool size is whatever you set: oms set codex-alt size=100. It is any number you can compare across your own accounts — oms cannot read it, because neither vendor reports an absolute quota, only a percentage. An account with no size set sorts after ones that have it.

Override the whole thing with an explicit order, oms priority codex-alt codex-main, and go back with oms priority --auto. Accounts you leave out still get used, just after every listed one. Priority is only about order — an account that could bill you is skipped by the guard below no matter where it sits.

When something stops working

Most of this is wiring into other programs — a status line path in Claude Code's settings, a hook in a plugin, a CLI per provider — and each of those is built to fail quietly, because a status line must not throw and a hook must not stand between you and your prompt. Quiet failure needs somewhere loud to ask:

$ oms doctor
  ok    oms                    /Users/you/.local/bin/oms
  ok    on PATH                /Users/you/.local/bin/oms
  FAIL  status line            /gone/oms is gone - run `oms install`
  ok    codex-main             5h 27%  7d 12%  (142/5000 cr)

It exits non-zero when anything is unreachable, so it works in a script too.

Before you hit the wall

Claude Code offers to switch accounts once you have already hit the limit, by which point there is no context budget left to write a handoff with. The plugin's UserPromptSubmit hook fires earlier: ten points below the block threshold, or whenever the last few hours' rate puts the ceiling under 45 minutes away, whichever comes first.

A percentage alone is the wrong trigger — 85% that has not moved all day is nothing to act on, and 70% burning 40% an hour is forty minutes from the wall. It names the account with room, says why it spoke, and then stays quiet for an hour unless usage moves another ten points. The warning asks you to act, so repeating it every prompt would eat the context it is warning you about.

Move the percentage trigger with "warn_at": N in ~/.oms/config.json.

Switching without losing the thread

A session cannot move between accounts: its transcript is written with the account that owns it, and codex and claude share no transcript format. So oms carries a brief rather than pretending to resume a conversation.

/oms:handoff                      # Claude writes a brief from the current session
oms run claude-alt --handoff      # it becomes the first prompt over there
oms auto codex --handoff          # works across vendors too, it is only text

oms handoff - takes a brief on stdin if you would rather write it yourself.

Settings

One vocabulary, translated into each provider's own spelling at launch. oms set is the only thing that writes a setting; oms config shows them all and what each account will launch as.

oms set all effort=high           # --effort high for claude, -c model_reasoning_effort for codex
oms set codex-main model=gpt-5.3-codex
oms set google-work model=gemini-3.1-pro-low effort=high
oms models google-work              # * marks its currently selected core model
oms set codex-main size=5000      # so the pick order knows which pool is smaller
oms set block_at=90               # global
oms set codex-main args=          # an empty value clears any key
per account
model that provider's own model name
effort low medium high xhigh max, and minimal on codex
context context window in tokens, where the provider takes one
size how big the plan's pool is, for ordering and for percentages
resets_on day of month the plan's credits reset
paid_overflow yes if hitting the ceiling on this account is charged
block_at override the global threshold for this account
args extra provider flags, appended raw, for anything not covered
name what the account is called, same as oms rename
global
block_at percentage at which an account that can bill is refused
warn_at percentage at which the hook offers a handoff
priority explicit pick order, or auto

A key the provider has no flag for is refused when you set it, rather than stored and silently dropped at launch — claude takes its context window from the model, so context is a codex setting only.

The paid-credit guard

An account is blocked when paid overflow is on for it AND usage is at or above block_at (default 95%).

  • A plan with no credit attached can't charge you — it just stops at the ceiling. oms never blocks those, so you use the whole thing.
  • A plan with credit attached can charge you the moment the window fills. That's what the guard is for. Codex reports its own credit balance; for Claude, set it yourself with --paid-overflow yes|no, since that flag is org-side.

oms also strips OPENAI_API_KEY / ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN from the launched process, so a stray key in your shell can't silently convert plan usage into metered usage.

Per-account override: "block_at": N in ~/.oms/config.json. One-off: oms run --force.

What this is not

It does not pool, share, or resell accounts, and it does not lift any account above its own limits. Every account is one you signed up for, it logs in through the vendor's own flow, and each plan's limit applies to it exactly as before. oms only tells you which of your plans has room and keeps you from running one into paid overage.

Any account you register should be one you are entitled to use on your own terms. If an account belongs to an employer or is billed to someone else, their policy governs it, not this tool.

FAQ

Is this a Claude Code account switcher or a Codex account switcher?

It covers those workflows, but its primary job is broader: subscription quota tracking, billing guards, exact-account routing, model defaults, and host integration across Claude Code, Codex CLI, Google Gemini / Antigravity, Pi, and partial Kiro support.

Does OMS upload OAuth tokens or subscription data?

The Python CLI does not read provider token bytes. The optional shared core owns OAuth and token refresh, stores its files privately under ~/.oms, binds to 127.0.0.1, and exposes a bearer-authenticated local facade to host adapters.

Can I configure models without switching my current Pi session?

Yes. /oms opens the global configuration picker. /oms use is the separate action that switches the current Pi session.

Why is a usage reading marked stale?

OMS only treats evidence as current while it is young relative to the quota window. It will not silently route based on an old number that may no longer describe the account.

Commands

oms login <provider> [<name>]        install if missing, log in, name, prime it
oms remove <name> [--purge]          unregister an account
oms add <name> <provider> [--dir PATH] [--paid-overflow yes|no]
oms install                          wire usage recording into ~/.claude/settings.json
oms status [--json]                  what every account has left
oms doctor                           check the wiring that fails silently
oms run [--force] <name> [args...]   launch one account
oms auto <provider> [args...]        launch the best account that can't bill you
oms auto <provider> --dry-run        say which one, and launch nothing
oms rename <old> <new>               rename an account, keeping its usage history
oms priority [<name>... | --auto]    show or set the order `oms auto` picks in
oms config                           every setting, global and per account
oms models [account|filter]          list available core models
oms set [<account>] key=value ...    change any setting
oms handoff [- | --clear]            carry a brief into the next session
oms run <name> --handoff             launch with that brief as the first prompt
oms env <name>                       print the export line for a shell
oms pi <account|gemini> [args...]    launch Pi with saved model and effort
oms core [install|start|stop|status] manage the shared provider core
oms core login <provider> [account]  authenticate and bind a core account
oms core sync                        apply quota and billing policy to credentials

Community and support

Released under the MIT License.