Skip to content
claudewheel.profile_data
Edit
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>::

/.claudewheel/ 0700, owner only /.claudewheel/token.json 0600, the token entry

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

python
def data_dir(self) -> Path

The dot-prefixed subdirectory holding this profile's data.

#token_file

python
def token_file(self) -> Path

The token entry file inside :attr:data_dir.

#exists

python
def exists(self) -> bool

True when this profile carries a claudewheel data directory.

#load

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

Parse the token entry. Missing -> {}; corrupt -> TokenStoreError.

#_corrupt_message

python
def _corrupt_message(self, reason: object) -> str

The one wording for an unusable token file.

#token

python
def token(self) -> str | None

The token string, or None when there is none stored.

#has_token

python
def has_token(self) -> bool

True when a token string is stored for this profile.

#expiry

python
def expiry(self) -> TokenExpiry | None

Computed expiry of the stored entry, or None when there is no entry.

#tier

python
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

python
def declares_plan(self) -> bool

True 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

python
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

python
def ensure_dir(self) -> None

Create :attr:data_dir if absent and set its mode explicitly.

#write_token

python
def write_token(self, token: str, *, expiry: TokenExpiryDisposition, plan: PlanTier, today: date | None=None) -> None

Write 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

python
def set_plan(self, plan: PlanTier) -> None

Merge 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

python
def remove_token(self) -> bool

Delete the token entry file. True when it existed.

#PROFILE_DATA_DIRNAME

python
PROFILE_DATA_DIRNAME = '.claudewheel'

#PROFILE_DATA_DIR_MODE

python
PROFILE_DATA_DIR_MODE = 448

#TOKEN_FILE_NAME

python
TOKEN_FILE_NAME = 'token.json'

#TOKEN_FILE_MODE

python
TOKEN_FILE_MODE = 384
Search