DEV Community

Adem İşler
Adem İşler

Posted on

Building a Local-First Quota Monitor and Account Switcher for Codex

Codex account management looks simple until several accounts have different quota windows, different reset times, and different local session states.

At that point, two questions become surprisingly difficult:

  1. Which account is actually usable right now?
  2. Which account is the local Codex installation currently using?

I originally wanted a small menu-bar utility that could answer those questions without opening dashboards, estimating usage from local activity, or manually replacing authentication files.

That became CodexControl: an open-source, local-first quota monitor and account switcher for Codex on macOS and Windows.

The product intentionally does only two things:

  • read live Codex quota state;
  • switch the active local Codex account.

Keeping the scope narrow made the interface simple. The implementation, especially cross-platform switching, was less simple than it first appeared.

Why a quota monitor needs live data

A usage estimate can be useful when an API exposes no authoritative quota information.

Codex already has account state that can be read through the authenticated local session, so I did not want to infer remaining capacity from token counts, request logs, or time spent working.

CodexControl uses each saved account's local authentication state to request live quota information directly.

That matters because an estimate can drift in several ways:

  • usage may happen from another computer;
  • background activity may not be visible locally;
  • different models or operations may consume capacity differently;
  • the server may adjust or normalize quota state;
  • cached responses may remain visible after the real window changes.

The app treats the provider response as the source of truth instead of trying to reconstruct it indirectly.

Quota is not a single percentage

A Codex account may expose more than one usage window.

The two windows most important to the app are:

  • a shorter five-hour window;
  • a longer seven-day window.

These windows can have very different states.

An account may still have weekly capacity while the short-term window is exhausted. Another account may have a nearly full short-term window but little weekly capacity remaining.

Collapsing both into one number would hide the operational decision the user needs to make.

CodexControl therefore keeps each window independent and shows its own:

  • remaining or used capacity;
  • reset time;
  • current availability;
  • normalized presentation state.

The app also preserves exact reset timestamps rather than displaying only a vague label such as “resets later.”

Accuracy became a first-order requirement

A quota monitor loses its value quickly if the numbers cannot be trusted.

I added several safeguards around live reads:

  • cache-bypassing network sessions;
  • repeated reads with equivalence checks;
  • rejection of inconsistent responses;
  • per-window normalization without merging independent values;
  • clearing stale snapshots after failed refreshes;
  • token refresh before retrying when required.

The most important principle was this:

A missing value is better than a confident but stale value.

If a refresh fails, continuing to show an old quota without a clear stale state can lead the user to switch to an account that is no longer usable.

The app prefers to expose the failure and request another refresh.

The account list is sorted for action, not decoration

A raw account list sorted alphabetically does not answer the main question.

The user is usually trying to decide which account to activate next.

CodexControl sorts accounts by practical usefulness so that currently usable accounts remain easier to reach. Presentation logic considers live window state, authentication health, and whether the account is active.

The list also supports local management actions:

  • add an account;
  • refresh quota;
  • reauthenticate;
  • relabel;
  • remove;
  • open the account directory;
  • switch the ambient Codex identity.

Periodic refresh runs every few minutes, but manual refresh remains available when the user needs an immediate answer.

Local accounts are stored as separate Codex homes

Each managed account has its own local Codex home and authentication state.

The ambient ~/.codex directory represents the account currently used by the normal Codex CLI and desktop workflow.

A simplified model is:

managed account A home ─┐
managed account B home ─┼─> CodexControl
managed account C home ─┘
                             |
                             v
                      ambient ~/.codex
Enter fullscreen mode Exit fullscreen mode

Switching means making the selected managed account become the ambient identity.

The exact work required depends on the platform and the current Codex Desktop implementation.

macOS: a native menu-bar workflow

The macOS application is built with SwiftUI and AppKit and lives in the menu bar.

That form factor matches the task:

  • glance at quota;
  • choose an account;
  • switch;
  • return to work.

There is no need for a large always-open dashboard.

On macOS, the app reads local Codex homes, refreshes account tokens when needed, fetches live quota, replaces the ambient session during a switch, and restarts Codex Desktop so the new identity is applied.

The project includes packaging, signing, notarization, and Homebrew cask support so the utility can behave like a normal desktop application rather than a script collection.

Windows revealed that switching is not one file copy

The first Windows implementation switched accounts by replacing only:

~/.codex/auth.json
Enter fullscreen mode Exit fullscreen mode

That was not enough for current Codex Desktop builds.

The CLI identity could change while the desktop application continued to show a login screen or a stale session.

The reason was that identity existed in multiple state layers.

Layer 1: CLI and global Codex state

The local Codex directory included:

auth.json
.codex-global-state.json
.codex-global-state.json.bak
Enter fullscreen mode Exit fullscreen mode

The global-state files could retain a previous creator_id even after the authentication file changed.

The switch flow therefore needed to rewrite the previous provider account ID with the target account ID in both global-state files.

Layer 2: Codex Desktop MSIX session state

The Windows desktop app also maintained browser and application session state inside its MSIX package cache:

%LOCALAPPDATA%\Packages\OpenAI.Codex_*\LocalCache\Roaming\Codex
Enter fullscreen mode Exit fullscreen mode

Updating the CLI files did not automatically replace this desktop session layer.

The visible symptoms were confusing:

  • the switch appeared to complete, but the desktop app still displayed the previous identity;
  • the desktop app reopened at a login screen;
  • the app terminated but failed to relaunch after a copy error;
  • a target account had valid CLI authentication but no matching desktop session yet.

Backing up desktop session state per account

The Windows restart flow now treats desktop session state as account-specific data.

During a switch it can:

  1. stop Codex Desktop;
  2. back up the current desktop session into the managed home of the account being left;
  3. update auth.json and global identity state;
  4. restore the target account's saved desktop session when available;
  5. relaunch Codex Desktop.

The backup and restore process operates per entry with logging rather than failing the entire switch on the first copy problem.

There is also an important first-use case.

A target account may not yet have a saved desktop-session snapshot. On the first switch, Codex Desktop may need to reconcile that account's state itself. Once the account has been active and its session has been backed up, later switches can restore it more consistently.

This was the clearest engineering lesson in the project:

Authentication state and application session state are related, but they are not the same thing.

Account identity cannot rely on email alone

Another subtle issue appeared when accounts shared identifying fields.

Using only email or authentication subject as the account key can collapse distinct provider accounts into one row.

CodexControl prefers the provider account ID and uses other fields only as fallbacks.

This matters when:

  • multiple accounts use the same email address;
  • an organization changes account metadata;
  • authentication records are rotated;
  • old and new managed homes coexist temporarily.

Stable identity should come from the most provider-specific identifier available.

Removed accounts should stay removed

The app discovers managed Codex homes from disk.

That creates a lifecycle problem: deleting an account from the visible list is not enough if an old duplicate home is rediscovered at the next startup.

The removal flow therefore needs to:

  • record the removed identity;
  • filter later discovery against removed identities;
  • remove duplicate managed homes for the same provider account;
  • avoid recreating an account from stale authentication data.

Deletion is a state-management feature, not merely a UI action.

Recovering from rotated refresh tokens

A user may have multiple local auth homes for the same account, and older homes may contain stale refresh tokens.

A failed refresh does not always mean the account itself is invalid. A newer matching home may contain the current credentials.

The recovery path can search matching local identities and use fresher authentication state before declaring that reauthentication is required.

This avoids forcing the user through login again when valid local credentials already exist.

At the same time, the app stops automatic retry loops for accounts already known to require reauthentication. Manual refresh and reauthentication remain explicit actions.

Cross-platform implementation without pretending the platforms are identical

The project uses different native approaches on each platform:

Platform Implementation
macOS SwiftUI and AppKit menu-bar application
Windows Python tray-first desktop application

It would have been possible to force both platforms into one cross-platform UI framework.

I chose not to make shared UI code a goal by itself.

The shared product behavior is more important:

  • account discovery;
  • identity normalization;
  • live quota reads;
  • token refresh;
  • sorting and presentation rules;
  • switching semantics;
  • synthetic demo data;
  • repository hygiene.

Platform-specific session handling remains platform-specific.

This is especially important on Windows, where MSIX package state and restart behavior require code that has no meaningful macOS equivalent.

Local-first does not mean offline

CodexControl is local-first, but it still contacts OpenAI to refresh authenticated quota information.

The distinction is about ownership and storage:

  • account files stay on the user's computer;
  • the app reads existing local Codex authentication state;
  • there is no CodexControl cloud account database;
  • screenshots and public examples use synthetic identities;
  • live tokens, snapshots, and desktop sessions are never intended for the repository.

The application stores its own local state under the normal platform application-data directories.

Credentials and session data deserve more careful treatment than ordinary preferences. The repository security policy explicitly tells users not to include auth.json, tokens, snapshots, private paths, or real account screenshots in public reports.

Synthetic demo data is part of security design

A quota dashboard naturally displays account names, emails, plan details, reset times, and usage values.

Real screenshots can leak more than expected.

The repository therefore uses synthetic accounts and snapshots for documentation and product images.

This is not just a marketing concern. Safe demo modes make it possible to:

  • reproduce UI states;
  • test sorting and warning behavior;
  • capture screenshots consistently;
  • review the project publicly;
  • avoid manually redacting every image.

A reliable synthetic-data path is often safer than asking contributors to “be careful” each time they take a screenshot.

Release engineering

CodexControl includes a tag-driven release workflow for both platforms.

The macOS path can:

  • import a Developer ID certificate when configured;
  • build the app;
  • sign the bundle;
  • notarize artifacts;
  • publish a ZIP and checksum.

The Windows path builds a standalone package and publishes its archive and checksum to the same GitHub release.

The project also includes:

  • a static product website;
  • direct release downloads;
  • a Homebrew cask;
  • a changelog;
  • platform-specific build and install scripts;
  • automated tests for account management and presentation behavior.

Distribution work is part of the product. A quota switcher that only runs from a development checkout would solve a much smaller problem.

What I learned

1. A narrow product can still require deep systems work

The interface answers two questions, but doing so reliably required authentication handling, live network reads, local state discovery, process restarts, Windows package caches, and release automation.

2. Accuracy is a user-experience feature

Repeated reads, stale-state clearing, and independent quota windows are not backend details. They determine whether the user trusts the next account-selection decision.

3. Account switching is distributed state synchronization

The active identity may exist in authentication files, global state, desktop caches, and running processes. Updating one layer does not guarantee the product has switched.

4. Cross-platform does not require identical implementations

A shared product contract can be more valuable than a shared UI framework. Native platform behavior should remain explicit where the operating systems differ.

5. Removal and recovery paths deserve first-class design

Adding an account is the easy path. Duplicate discovery, stale refresh tokens, deleted identities, failed restarts, and first-time desktop sessions are where the product becomes dependable.

Current scope

CodexControl remains intentionally Codex-specific.

It is not a general multi-provider dashboard, billing platform, or browser-based account manager.

It is for people who actively use multiple Codex accounts and need a fast local answer to:

  • which account has usable capacity;
  • when its quota resets;
  • which account is currently active;
  • how to switch without manually repairing local state.

The project is open source and supports macOS and Windows.

GitHub logo ademisler / codexcontrol

Local-first Codex quota tracker and multi-account switcher for macOS and Windows

CodexControl

CodexControl logo

CodexControl demo UI

Local-first Codex quota tracking and account switching for macOS and Windows.

Website · GitHub · Releases · Security

macOS 14+ Windows Local-first MIT License

CodexControl is a focused desktop app for people who actively manage multiple OpenAI Codex accounts.

It does two things well:

  • shows live quota directly from OpenAI
  • switches the active Codex account used by the local CLI

Install

Homebrew

brew install --cask ademisler/tap/codexcontrol
Enter fullscreen mode Exit fullscreen mode

Direct Download

Why CodexControl

Most tools around this workflow fall into one of these categories:

  • multi-provider dashboards with too much surface area
  • browser-driven quota trackers with extra runtime overhead
  • scripts that estimate usage instead of reading the live account state
  • switchers that do not tell you which account is actually usable right now

CodexControl is intentionally narrower:

  • Codex-only
  • local-first
  • fast to scan
  • built around real quota windows and active-account control

Core Capabilities













Capability What it does
Live quota reads





I would be interested in feedback on a few areas:

  • Which quota signals are most useful when choosing between accounts?
  • How should the UI represent partial availability across short and long windows?
  • Have you encountered other desktop apps where authentication and browser-session state diverge?
  • Which local-first account-management workflows deserve better tooling?

This is the fourth article in my series documenting the products I have built. The next one covers Myna Player and the problem of generating context-aware subtitles without letting them arrive late.

Top comments (0)