Skip to content
claudewheel.tokens
Edit
On this page

The OAuth token entry format and the plan tier: assemble an entry, compute its expiry from the dates it carries, resolve a declared plan from the closed list of them, and map its two fields onto the environment variables Claude Code reads.

#claudewheel.tokens

#claudewheel.tokens

The OAuth token entry format: build one, date it, and read its tier fields.

The entry itself is stored per profile by :mod:claudewheel.profile_data, one file inside each profile directory. This module owns nothing on disk -- only the shape of an entry and what its fields mean.

It also owns the plan tier: :class:PlanTier and the closed :data:PLAN_TIERS list every writer resolves against, so the two stored fields are only ever written as one coherent pair.

#PlanTier

One declarable plan: a label, and the two fields it stores.

The pair is the unit. Claude Code reads a subscription type and a rate-limit tier and combines them (a Max account on the 20x tier is max + default_claude_max_20x; Team is team + default_claude_max_5x; Pro has no rate-limit string at all), so declaring one field without the other describes no real account. Every writer therefore passes a whole :class:PlanTier.

Both fields are validated at construction against the sets Claude Code actually compares against: a value it does not recognize is inert there -- the tier resolves to null exactly as an absent value would -- so an unrecognized value is a hard error naming the accepted ones, never a silently stored string.

#fields

python
def fields(self) -> dict[str, str]

The entry fields this plan writes, omitting a field it does not carry.

#apply_plan

python
def apply_plan(entry: dict[str, Any], plan: PlanTier) -> None

Write plan's fields into entry, in place.

Both plan fields are cleared first, so a plan that carries no rate-limit tier (Pro, Enterprise) leaves none behind. Otherwise declaring Pro over Max 20x would store pro beside default_claude_max_20x -- a pair no account has, injected into the launch environment as if it did.

#plan_keys

python
def plan_keys() -> list[str]

The declarable plan keys, in picker order.

#plan_by_key

python
def plan_by_key(key: str) -> PlanTier

Resolve a plan key to its :class:PlanTier.

The single door every writer goes through. An unknown key is a :class:ValueError naming the valid ones.

#entry_declares_plan

python
def entry_declares_plan(entry: dict[str, Any]) -> bool

True when entry declares a plan.

Keyed on the subscription type: it is the field every plan carries, and the one Claude Code's entitlement checks read. An entry holding only a rate-limit tier declares no plan -- that is the shape a hand-edit or an older claudewheel could leave behind, and it is exactly as unusable as an empty entry.

#TokenExpiryDisposition

How a token's expiry is recorded when the entry is written.

The caller MUST choose one explicitly -- there is no default -- so token lifetime is never silently fabricated.

  • TTL: a claude setup-token, genuinely valid for TOKEN_TTL_DAYS.

The entry gets created (today) and expires_at (created + TTL).

  • UNKNOWN: an externally-issued token whose expiry we cannot know.

The entry gets created (today) and the expiry_unknown marker, and NO expires_at -- expiry is reported as unknown, never assumed.

#parse_entry

python
def parse_entry(entry: object) -> str | None

Extract the token string from a token entry.

An entry is a dict like {"token": ..., "created": ..., "expires_at": ...}. Returns None if the entry is empty, absent, or unrecognized.

#TokenExpiry

Computed token lifetime: creation date, expiry date, days remaining.

remaining_days is None only for entries marked with an unknown expiry disposition -- a distinct, honest "we don't know" that consumers must handle separately from the "assume fresh" fallback (which reports a concrete TOKEN_TTL_DAYS).

#entry_expiry

python
def entry_expiry(entry: dict[str, Any], today: date | None=None) -> TokenExpiry

Compute a dict token entry's creation date, expiry date, and days left.

Precedence: an explicit unknown-expiry marker yields (None, None, None); else explicit "expires_at" ISO date; else "created" + TOKEN_TTL_DAYS. Unparseable or absent fields yield (None, None, TOKEN_TTL_DAYS), matching the historical health-check behavior of assuming a fresh token.

The entry shape is the single per-profile record: the token string, its creation/expiry dates, and the plan-tier fields. Every store that holds one computes expiry through here.

#entry_plan_env

python
def entry_plan_env(entry: dict[str, Any], *, source: object) -> dict[str, str]

Map a token entry's plan-tier fields to Claude Code's env vars.

Reads the subscriptionType and rateLimitTier fields, mapping each present one to its CLAUDE_CODE_* variable. An entry declaring neither yields an empty dict.

Declared values are validated against the sets Claude Code actually compares against, because a value it does not recognize behaves exactly like no value at all -- the tier resolves to null and the failure looks identical to not having configured anything. Raises :class:ValueError naming the offending field, source (where the value was read from) and the accepted values.

#build_entry

python
def build_entry(token: str, *, expiry: TokenExpiryDisposition, plan: PlanTier, today: date | None=None) -> dict[str, Any]

Assemble a token entry dict in the canonical shape.

The one description of the entry format: token plus created, then either expires_at (a TTL disposition) or the expiry_unknown marker (an externally-issued token), plus the plan fields.

plan is required and has no default: a token written without a stated plan would launch Claude Code with the tier resolved to null, and there is no value to guess from. Writing a token also RE-STATES the plan -- the entry is rebuilt, so a replaced token can never inherit the plan declared for the one before it.

#TokenStoreError

Raised when a token entry cannot be read/parsed and resolution cannot proceed.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • rlsbl Release orchestration and project scaffolding CLI that bumps versions, validates a structured JSONL changelog, tags only the commit CI verified, and publishes to npm, PyPI, Go and more
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search