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
def fields(self) -> dict[str, str]The entry fields this plan writes, omitting a field it does not carry.
#apply_plan
def apply_plan(entry: dict[str, Any], plan: PlanTier) -> NoneWrite 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
def plan_keys() -> list[str]The declarable plan keys, in picker order.
#plan_by_key
def plan_by_key(key: str) -> PlanTierResolve 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
def entry_declares_plan(entry: dict[str, Any]) -> boolTrue 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 forTOKEN_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
def parse_entry(entry: object) -> str | NoneExtract 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
def entry_expiry(entry: dict[str, Any], today: date | None=None) -> TokenExpiryCompute 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
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
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.