claudewheel v0.24.2 /claudewheel.cli
Edit
On this page

CLI argument parsing, subcommand routing, and launch orchestration for the claudewheel command-line interface.

#claudewheel.cli

#claudewheel.cli

CLI argument parsing, subcommand routing, and launch orchestration.

#_do_uninstall

python
def _do_uninstall(locator: 'BinaryLocator', version: str) -> int

Delete an installed Claude Code version binary.

Refuses to delete the version the claude symlink currently points to, since that would break the default claude command. Returns a process exit code.

#_do_reset_options

python
def _do_reset_options(ws: 'Workspace') -> int

Delete options.json so it regenerates from defaults on next run.

Does NOT instantiate AppConfigStore -- the next normal run will recreate options.json via _ensure_dir. Idempotent: missing file is not an error.

#_do_show

python
def _do_show(cfg: 'AppConfigStore') -> int

Print a git-status-like summary of last_config, segments, theme, and recent dirs.

#_write_tier_stub

python
def _write_tier_stub(ws: 'Workspace', profile: str | None, config_dir: str | None) -> None

Write a rateLimitTier stub into .credentials.json if tokens.json has tier data.

This lets downstream tools (e.g. howmuchleft) read the tier from .credentials.json even when auth is via CLAUDE_CODE_OAUTH_TOKEN. Short-circuits if .credentials.json already has the same tier value. A corrupt tokens.json raises TokenStoreError (surfaced cleanly by the launch handler); the .credentials.json write remains best-effort.

#_do_launch_sequence

python
def _do_launch_sequence(ws: 'Workspace', locator: 'BinaryLocator', cfg: 'AppConfigStore', selections: dict[str, str | None], extra_flags: list[str] | None=None, interactive: bool=True, metadata: dict[str, dict[str, dict[str, Any]]] | None=None, client: str=DEFAULT_CLIENT, passthrough: list[str] | None=None) -> None

Run health check, hooks, save state, resolve, and exec. Does not return on success.

#_handle_health

python
def _handle_health(ws: 'Workspace') -> int

Run diagnostic health checks and print results.

#_handle_config

python
def _handle_config(ws: 'Workspace') -> int

Open the config directory in the user's preferred editor.

#_handle_versions

python
def _handle_versions(locator: 'BinaryLocator') -> int

List installed Claude Code versions, marking the current symlink target.

#_handle_install

python
def _handle_install(locator: 'BinaryLocator', version: str) -> int

Download and install a specific Claude Code version.

#_handle_uninstall

python
def _handle_uninstall(locator: 'BinaryLocator', version: str) -> int

Uninstall a specific Claude Code version binary.

#_handle_reset_options

python
def _handle_reset_options(ws: 'Workspace') -> int

Delete options.json so defaults regenerate on next run.

#_handle_new_profile

python
def _handle_new_profile(ws: 'Workspace', locator: 'BinaryLocator') -> int

Run the create-profile flow as one continuous alt-screen session.

Mirrors the TUI path: wizard form, auth forms, and summary page all render borrowed in a single alt-screen raw session on a CLI-owned terminal. After the session ends, the summary and auth outcome are printed to stdout as a persistent record.

#_handle_delete_profile

python
def _handle_delete_profile(ws: 'Workspace', name: str, force_delete: bool, force_delete_data: bool) -> int

Delete a profile via ProfileStore. The running check is CLI policy.

#_handle_rename_profile

python
def _handle_rename_profile(ws: 'Workspace', old: str, new: str) -> int

Rename a profile: validate inputs, then delegate to ProfileStore.rename.

The charset, name-collision (options + tokens), and running checks stay here as CLI policy -- they produce clean, targeted messages. The store enforces dir-existence and the 'default' reservation as a backstop; its ValueErrors are mapped to the same error-print + exit-1 style.

#_handle_check_tokens

python
def _handle_check_tokens(ws: 'Workspace') -> int

Validate stored tokens for all discovered profiles against the Anthropic API.

#_handle_fix_auth

python
def _handle_fix_auth(ws: 'Workspace', name: str) -> int

Remove a credential shadow, or a stale token entry for a missing profile.

Two repair kinds through one surface:

  • existing profile: strip session credentials that shadow a long-lived token
  • missing profile: remove a stale tokens.json entry whose profile dir is gone

#_handle_show

python
def _handle_show(ws: 'Workspace') -> int

Print a summary of current selections, theme, and recent directories.

#_handle_migrate

python
def _handle_migrate(ws: 'Workspace', src: str, dst: str, uuid: str) -> int

Move session data files between profiles, optionally filtered by UUID.

#_handle_stats

python
def _handle_stats(ws: 'Workspace') -> int

Report shared-store statistics and optionally clean up legacy data.

#_handle_mv

python
def _handle_mv(ws: 'Workspace', old: str, new: str, post_hoc: bool) -> int

Rename a project directory and migrate its session data.

#_handle_import

python
def _handle_import(ws: 'Workspace', source: str, from_: list[str], to: list[str], reid: bool) -> int

Import session data from an external Claude Code directory.

#_handle_deploy_hooks

python
def _handle_deploy_hooks(ws: 'Workspace', name: str, all: bool, force_overwrite: bool) -> int

Deploy built-in hook scripts to the scripts directory.

#_handle_patch_profiles

python
def _handle_patch_profiles(ws: 'Workspace') -> int

Reconcile every managed profile and shared-settings.json to exact canonical.

Delegates to the unified reconcile core. This PRUNES each target's guardrail sections (the entire hooks structure, the disallowedTools list, and permissions deny/ask) to EXACTLY the canonical model, removing drift and any user-added extras -- the old additive, extras-preserving semantics are gone. Also deploys any missing guardrail hook scripts. The 'default' profile (~/.claude) is never read from or written to.

Declared consequential, like reconcile-permissions it delegates to: the pruning is unrecoverable, so the framework confirms before dispatch and refuses outright without a terminal unless --approve-consequential is passed. --dry-run previews and is never gated.

#_handle_reconcile_permissions

python
def _handle_reconcile_permissions(ws: 'Workspace', profile: str) -> int

Reconcile every managed target to EXACTLY the canonical guardrail model.

Delegates to the unified reconcile core. Makes each target's hooks, the disallowedTools list, and permissions deny/ask EXACTLY canonical (allow keeps only its non-conflicting entries), pruning all drift and user-added extras -- the old additive, extras-preserving behavior is gone. The 'default' profile (~/.claude) is never read from or written to.

The hand-rolled --dry-run/--apply pair this command used to require is gone: --dry-run is now the framework's, and it is the only mode flag. The explicit-intent half of that pair is not gone, though -- the command declares itself consequential, so the framework confirms before dispatch and refuses a bare run on a non-interactive stdin with "pass --approve-consequential to confirm". The pruning is exact and nothing reconstructs a removed entry, which is what earns the interruption; the informative preview is still the per-target diff --dry-run prints, and --dry-run is never gated.

#_handle_permission_add

python
def _handle_permission_add(ws: 'Workspace', category: str, rule: str, profile: str, all_profiles: bool) -> int

Add a permission rule to the specified category for one or all profiles.

#_handle_permission_remove

python
def _handle_permission_remove(ws: 'Workspace', category: str, rule: str, profile: str, all_profiles: bool) -> int

Remove a permission rule from the specified category for one or all profiles.

#_handle_permission_list

python
def _handle_permission_list(ws: 'Workspace', profile: str, all_profiles: bool, format: str, category: str) -> int

List permission rules for one or all profiles in the chosen format.

#_resolve_resume_title

python
def _resolve_resume_title(ws: 'Workspace', resume_val: str, directory: str) -> str

Resolve a --resume argument to a session UUID.

If resume_val is UUID-shaped it is returned unchanged. Otherwise it is treated as a session title (Claude Code accepts either). Titles are resolved by scanning the current directory's project dir first, then all project dirs. Exactly one match rewrites the value to that session's UUID and the caller proceeds through the normal UUID machinery. Zero or multiple matches print guidance and exit nonzero.

#_check_resume_session

python
def _check_resume_session(ws: 'Workspace', session_id: str, directory: str) -> None

Intercept --resume to detect and offer to fix directory renames.

When a session exists under an old encoded path (because the project directory was renamed), this function detects the mismatch and offers to move all sessions to the new path via run_mv.

Returns normally when no interception is needed (session found under current directory, or sessions successfully moved). Calls sys.exit(1) when the session cannot be resumed from here.

#_check_cont_session

python
def _check_cont_session(ws: 'Workspace', directory: str) -> None

Intercept --cont to detect and offer to fix directory renames.

When the current directory has no sessions but an orphaned project directory exists under the same parent (original cwd no longer on disk), this function offers to move those sessions to the current directory via run_mv.

#_reject_claude_only_overrides

python
def _reject_claude_only_overrides(client_val: str, segment_overrides: dict[str, Any]) -> None

Hard-error on explicit claude-only overrides combined with a non-claude client.

version and mcp=strict are claude-client-only inputs. An ambient value (remembered in last_config or a config default) is silently ignored for non-claude clients; but an explicit, same-invocation override (a segment flag or -s key=value) alongside a non-claude --client is contradictory intent and is rejected here, where the selection's provenance (an explicit override) is known -- the adapter downstream cannot tell explicit from ambient.

#_handle_launch

python
def _handle_launch(ws: 'Workspace', locator: 'BinaryLocator', cont: bool, resume: str, print_prompt: str, picker: bool, profile: str, github: str, model: str, directory: str, mcp: str, permissions: str, set: list[str], client: str | None) -> int

Handle the launch subcommand: run the TUI or skip it when args suffice.

#_inject_launch

python
def _inject_launch(argv: list[str]) -> list[str]

Return argv with the "launch" subcommand injected when appropriate.

argv includes argv[0] (the program name). When no subcommand is given, or the first token that is not a framework-reserved global flag is neither a known subcommand nor an app-level flag, the "launch" subcommand is injected at that position so the interactive TUI starts. App-level flags (see _APP_LEVEL_FLAGS) and known subcommands are left untouched.

#_bind

python
def _bind(handler: Callable[..., int], *pre: Any) -> Callable[..., int]

Pre-bind leading positional dependencies (workspace/locator) to a handler.

strictcli dispatches handlers with keyword arguments (handler(ctx, **parsed)) and builds the schema from the declared Flag/Arg objects -- NOT from the handler signature. The signature is what strictcli's guard v2 validates the declaration against. So the returned wrapper:

  • forwards the pre-bound deps plus parsed kwargs to the real handler,
  • binds the dispatch context to :mod:claudewheel.effects for the length

of the call, which is what makes --dry-run record every mutation instead of performing it,

  • carries an explicit __signature__: the real handler's parameters with

the pre-bound positionals dropped and the framework's context slot put back in front.

That __signature__ is the point. The wrapper is physically (ctx, **kwargs), and a bare **kwargs handler is exactly the hole strictcli's guard v2 closes -- it would have to declare forwarding= and waive the signature cross-check for all 24 commands. Presenting the wrapped handler's real signature instead means every flag and arg is validated against a real parameter, which is the "declare everything" guarantee this wrapper used to opt out of.

We deliberately do NOT use functools.wraps: it would set __wrapped__, and inspect.signature follows that chain back to the real (ws-bearing) signature, re-triggering validation against parameters the framework never supplies.

#_build_app

python
def _build_app(ws: 'Workspace', locator: 'BinaryLocator') -> App

Build the strictcli App with all subcommands registered.

#main

python
def main() -> None

CLI entry point that parses arguments and dispatches to subcommands or the TUI.

Search