On this page
Form-style TUI wizard for creating new Claude Code profiles.
#claudewheel.wizard
#claudewheel.wizard
Interactive form wizard for creating and configuring new profiles.
#WizardResult
Collected values from a completed profile wizard run.
#_validate_name
def _validate_name(name: str, existing_profiles: list[str]) -> str | NoneReturn error message, or None if valid.
#_build_fields
def _build_fields(existing_profiles: list[str]) -> list[FormField]Build the ordered list of wizard form fields.
#_build_result
def _build_result(values: dict[str, object]) -> WizardResultConstruct a WizardResult from submitted form values.
#_cancelled_result
def _cancelled_result() -> WizardResultReturn a cancelled WizardResult with zero-value fields.
#run_profile_wizard
def run_profile_wizard(existing_profiles: list[str], theme, terminal) -> WizardResultRun the profile creation form and return the user's choices.
The form renders on terminal with theme colors via the ui widget layer: fullscreen, borrowed when the terminal is already raw.
#_load_shared_settings
def _load_shared_settings() -> dictLoad shared-settings.json, falling back to canonical defaults if missing.
#_set_onboarding_flag
def _set_onboarding_flag(config_dir: str) -> NoneMerge hasCompletedOnboarding: true into {config_dir}/.claude.json.
Claude Code gates on this flag in interactive mode -- it is normally set by CC's own OAuth success handler, but claudewheel bypasses that when injecting tokens via CLAUDE_CODE_OAUTH_TOKEN. Read-merge-write preserves any other metadata CC has already written (machineID, cachedGrowthBookFeatures, etc.). Corrupt or missing files are handled gracefully. If the config directory itself doesn't exist, this is a no-op (nothing to write to).
#create_profile
def create_profile(result: WizardResult, cfg: ConfigManager) -> list[str]Execute the profile creation based on wizard results.
Returns the summary lines describing what was created; presentation is the caller's job (the TUI shows a fullscreen page, the CLI prints them).
#_find_claude_binary
def _find_claude_binary() -> str | NoneLocate the Claude Code binary. Returns the path or None.
#run_auth_flow
def run_auth_flow(config_dir: str, profile_name: str, theme, terminal, skip_label: str='Skip for now') -> strPrompt the user to set up authentication for a newly created profile.
Presents a selection form with three choices: session login (browser-based), long-lived token, or skip. After picking a method, a second form asks which browser to open the auth URL in (or to suppress browser opening and copy the URL manually). The browser chosen in the last successful auth is remembered in state.json and pre-focused on the next run. Returns one of:
"authenticated"-- auth was set up successfully (tokens: validated
against the API before saving)
"unverified"-- a token was saved WITHOUT validation (the probe
was unreachable or inconclusive and the user explicitly chose to save)
"skip"-- the user explicitly chose to skip"cancel"-- the user cancelled a form (Esc/Ctrl-C)"failed"-- auth was attempted but did not complete
This function is safe to call after create_profile() -- auth failure never prevents profile creation. The forms render on terminal with theme colors; a terminal that is already raw is borrowed (the forms render as pages in the existing screen).
#_apply_browser_env
def _apply_browser_env(env: dict[str, str], browser: str) -> NoneSet BROWSER in env from the browser-form selection.
browser is either a browser binary path or "copy". Claude Code spawns $BROWSER ; BROWSER=false makes that fail, so claude falls back to printing the URL for manual copying.
#_capture_tier_from_credentials
def _capture_tier_from_credentials(credentials: Path, profile_name: str) -> NoneRead rateLimitTier/subscriptionType from .credentials.json and store in tokens.json.
Best-effort: silently skips if the credentials file cannot be parsed or the expected fields are absent (older Claude Code versions omit them).
#_auth_session_login
def _auth_session_login(config_dir: str, profile_name: str, browser: str, terminal) -> boolRun claude auth login with CLAUDE_CONFIG_DIR and BROWSER set.
The whole body runs inside terminal.cooked() so the claude subprocess and all prints see a real cooked terminal; raw mode (and the alt screen, if any) is restored on exit.
#_read_pasted_token
def _read_pasted_token(prompt: str) -> str | NoneRead a manually pasted token from input().
Removes ALL whitespace, including linebreaks and spaces embedded by line-wrapped terminal copies. Returns None on EOF/Ctrl-C; an empty string when nothing (or only whitespace) was entered.
#_capture_setup_token
def _capture_setup_token(config_dir: str, browser: str) -> str | NoneRun claude setup-token under a PTY and scrape the token.
Must run inside a cooked window: run_under_pty proxies the real terminal (it sets its own raw mode internally and restores it). The user interacts with setup-token normally while ALL output is captured; the token is then extracted from the capture -- no paste needed. Returns the token, or None on failure.
If extraction fails, that is a hard, explicit situation: the user is told the scrape failed and offered a manual paste as a clearly labeled recovery step -- not a silent fallback. Empty input aborts.
#_save_token
def _save_token(profile_name: str, token: str) -> boolWrite the token via add_token; print and return False on OSError.
#_auth_long_lived_token
def _auth_long_lived_token(config_dir: str, profile_name: str, browser: str, theme, terminal) -> strRun claude setup-token, scrape the token, validate it, save it.
Every token (scraped or manually recovered) is probed against the API BEFORE being saved. Returns one of:
"authenticated"-- the probe returned VALID and the token was saved"unverified"-- the probe was UNREACHABLE/INDETERMINATE and the
user explicitly chose to save the unvalidated token
"failed"-- anything else. A token the API rejects (401) is NEVER
saved: one manual re-paste is offered (the scrape may have picked a stale frame), then the flow fails hard.