On this page
claudewheel's per-profile data store: the dot-prefixed directory inside each profile holding its OAuth token entry, that entry's format, and the owner-only modes both are written at.
#claudewheel.profile_data
#claudewheel.profile_data
claudewheel's own data, stored inside each profile directory.
A profile directory IS a Claude Code config dir: Claude Code owns everything in it, and claudewheel passes the path to it as CLAUDE_CONFIG_DIR. What claudewheel knows about a profile -- today the OAuth token and the plan-tier fields -- lives in one dot-prefixed subdirectory inside that same directory, :data:PROFILE_DATA_DIRNAME, so the profile and everything the launcher knows about it travel together: a rename moves the directory and the data with it, a deletion removes both, and nothing outside can be left pointing at a profile that no longer exists.
Layout, for a profile whose config dir is <profile_dir>::
The token entry is a single JSON object (never a name-keyed map -- the file already belongs to exactly one profile) in the shape :func:build_entry assembles:
================= ======================================================== token the OAuth token string created ISO date the entry was written expires_at ISO expiry date; present for a TTL disposition expiry_unknown true for an externally-issued token instead rateLimitTier declared plan tier, validated against Claude Code's set subscriptionType declared subscription type, validated the same way ================= ========================================================
Modes are set explicitly rather than inherited from the umask. The token file is written 0600 from creation by the secret writer, and the subdirectory is chmod'd 0700 every time it is ensured: a world-readable parent would expose the file's existence, its size and its mtime -- and on a directory whose mode was never stated, that is what the default gives.
#ProfileDataStore
Path-injected read/write facade over ONE profile's claudewheel data.
profile_dir is the profile's config dir (its CLAUDE_CONFIG_DIR); every path is derived from it and nothing here reads a module path constant or calls Path.home(). Construction is pure value assembly -- no directory is created until a write happens.
Read APIs raise :class:~claudewheel.tokens.TokenStoreError on a corrupt or unreadable token file; a missing file is not an error and reads as "this profile has no claudewheel data".
#data_dir
def data_dir(self) -> PathThe dot-prefixed subdirectory holding this profile's data.
#token_file
def token_file(self) -> PathThe token entry file inside :attr:data_dir.
#exists
def exists(self) -> boolTrue when this profile carries a claudewheel data directory.
#load
def load(self) -> dict[str, Any]Parse the token entry. Missing -> {}; corrupt -> TokenStoreError.
#_corrupt_message
def _corrupt_message(self, reason: object) -> strThe one wording for an unusable token file.
#token
def token(self) -> str | NoneThe token string, or None when there is none stored.
#has_token
def has_token(self) -> boolTrue when a token string is stored for this profile.
#expiry
def expiry(self) -> TokenExpiry | NoneComputed expiry of the stored entry, or None when there is no entry.
#tier
def tier(self) -> tuple[str | None, str | None]The declared (rateLimitTier, subscriptionType), unvalidated.
The raw pair as stored, for reporting surfaces that show what is on disk rather than resolving it into launch environment variables (which validates -- see :meth:plan_env).
#declares_plan
def declares_plan(self) -> boolTrue when this profile's entry declares a plan.
The pre-launch prompt's question: a profile launching on a stored token without one leaves Claude Code's tier null.
#plan_env
def plan_env(self) -> dict[str, str]The declared plan tier as Claude Code env vars, validated.
An unrecognized value is a :class:ValueError naming the field, the file and the accepted values -- never a silently ignored field, because Claude Code treats a value it does not know exactly like no value.
#ensure_dir
def ensure_dir(self) -> NoneCreate :attr:data_dir if absent and set its mode explicitly.
#write_token
def write_token(self, token: str, *, expiry: TokenExpiryDisposition, plan: PlanTier, today: date | None=None) -> NoneWrite the token entry, replacing whatever was there.
expiry is required: the caller must choose how the token's lifetime is recorded (see :class:~claudewheel.tokens.TokenExpiryDisposition), so a lifetime is never silently fabricated.
plan is required too, and for the same reason -- no code path may put a token on disk without stating the plan it belongs to. Because the entry is rebuilt rather than merged into, replacing a profile's token invalidates the plan declared for the previous one: the caller states a plan again or writes nothing.
The directory is created at 0700 and the file written 0600 from creation.
#set_plan
def set_plan(self, plan: PlanTier) -> NoneMerge plan's fields into the entry, creating it if absent.
The declaration path for a profile that already holds a token: the token, its dates and everything else in the entry are left alone. A corrupt entry file raises :class:~claudewheel.tokens.TokenStoreError rather than being overwritten.
#remove_token
def remove_token(self) -> boolDelete the token entry file. True when it existed.
#PROFILE_DATA_DIRNAME
PROFILE_DATA_DIRNAME = '.claudewheel'#PROFILE_DATA_DIR_MODE
PROFILE_DATA_DIR_MODE = 448#TOKEN_FILE_NAME
TOKEN_FILE_NAME = 'token.json'#TOKEN_FILE_MODE
TOKEN_FILE_MODE = 384