Skip to content
Configuration
Edit
On this page

How the claudewheel configuration system works: the root file layout, per-profile directories and their claudewheel data, segments and options, discovery with its caches -- including model discovery from the Anthropic API, the release-date ordering it feeds, and the built-in minimum-version table that dims models needing a newer Claude Code than the version you have selected or installed (a dimmed model refuses to launch, while a version neither selection nor symlink can resolve dims nothing) -- the migration framework with the one-time versioned fixes it replays from the list registered in the code, schema versioning, flag-driven launches, and how interactivity is derived from a controlling terminal.

#Configuration

claudewheel's configuration lives in four JSON files under ~/.claudewheel/. On first run, each file is created from built-in defaults. On subsequent runs, a migration system adds new keys and applies one-time schema fixes without overwriting existing user values.

#File layout

All configuration lives under ~/.claudewheel/ (overridable via the CLAUDEWHEEL_CONFIG_DIR environment variable).

File layout
FilePurpose
config.jsonGlobal settings: enabled segments, theme, default flags, default client, minimap mode, health check toggle, and the internal _schema_version counter
segments.jsonSegment definitions: one entry per segment with its key, label, layout constraints (min/max width, wrap, searchable, creatable, freeform), and behavior flags (required, tab_advances, show_options)
options.jsonPer-segment option data: static values, pinned values, discovery configuration, and segment metadata
state.jsonRuntime state: last-selected values (last_config), recent directories, launch count, auth browser preference, per-project hook approvals, and the npm version and model list caches
themes/dark.jsonDark theme color definitions
themes/light.jsonLight theme color definitions
shared-settings.jsonCanonical shared settings applied to all profiles: hooks, disallowedTools, and profileDefaults (permissions deny/ask arrays and the canonical settings keys)

Additionally, profiles/<name>/ directories hold per-profile settings and credentials plus claudewheel's own .claudewheel/ data directory (the stored OAuth token), and shared/ holds session data symlinked from each profile.

#config.json

The main configuration file controls global behavior.

config.json
KeyTypeDefaultDescription
themestring"auto"Theme selection: "dark", "light", or "auto" (detects terminal background)
enabled_segmentsarray["profile", "github", "version", "model", "directory", "mcp", "permissions"]Which segments appear in the TUI bar, in order
default_flagsarray["--dangerously-skip-permissions"]Flags passed to every Claude Code launch
health_check_on_launchbooltrueRun diagnostic health checks before each launch
minimapstring"auto"Minimap visibility: "auto" (only when scrolling) or "always"
default_clientstring"claude"Pre-selected client for the interactive launcher and fallback for non-interactive launches
_schema_versionint0Internal migration counter (do not edit manually)

Remove a segment key from enabled_segments to hide it from the bar entirely.

#Segments and options

The TUI renders a horizontal bar of segments. Each segment has a key (e.g. profile, model, directory), a display label, and a list of selectable options. The option list is assembled from multiple sources with a configurable merge order.

#Option collections

Each segment maintains four option collections:

  • pinned -- values explicitly added by the user (via the wizard, the +

creation UI, or manual edits to options.json). Pinned values survive across restarts and are never overwritten by discovery.

  • discovered -- values found at runtime by the segment's discovery function

(scanning directories, querying npm, enumerating profiles, etc.).

  • defaults -- static fallback values from the built-in DEFAULT_OPTIONS in

defaults.py. These are the baseline options that ship with claudewheel. The model segment is the exception: its defaults collection comes from the accumulated values list in options.json (see "Model discovery" below), and the built-in list is only the seed that list starts from.

  • ephemeral -- values added during the current session only (e.g. a

freeform-typed directory path). Not persisted.

The final option list is built by concatenating collections in a configurable order (the collection order), deduplicating (first occurrence wins), and optionally sorting. Different segments use different merge strategies:

Option collections
SegmentCollection orderSort
versionpinned, discovered, defaultssemver descending
profilepinned, discovered--
modelpinned, discovered, defaultsrelease date descending
mcppinned, defaults--
permissionspinned, defaults--
githubpinned, discovered--
directorypinned, discovered, defaults--

#Discovery types

Segments with dynamic content use a discovery function registered in the DISCOVERY_REGISTRY. Discovery runs at startup; slow discoveries (those that make network calls) run in a background thread and merge results into the bar when ready.

Discovery types
Discovery typeSegmentsWhat it doesSlow?
claude_config_scanprofileEnumerates profiles from the ProfileStoreno
npm_and_localversionFetches recent versions from npm, merges with locally installed binariesyes
directory_scandirectoryScans parent directories (~/Projects, ~/repos, etc.) and validates recent dirs from stateno
gh_authgithubQueries gh auth status for logged-in GitHub accountsyes
anthropic_modelsmodelLists the models the account may use from the Anthropic APIyes
state_field--Merges state-tracked values with static defaultsno

Slow discovery results are cached, each with a 1-hour TTL in state.json: the npm version list under npm_versions_cache, the model list under model_list_cache. While a cache is warm, startup reads it instead of the network; when a refresh fails, the cached answer is used however stale it is.

#Model discovery

anthropic_models reads GET /v1/models with the OAuth token stored in a profile, trying the last-used profile first and falling through to the others when one is rejected (a 401) or rate limited (a 429, which is per-account, so another account may still answer). A profile that stores no token is never tried, and any other HTTP status, like an offline machine, ends the refresh rather than working through every token. It is additive in both directions:

  • Every id it reports joins options.json's model values list once, at the

end, and stays there -- so a model discovered while online is still offered when offline, and a model the API stops listing is never taken away.

  • Each id's release date is recorded as created_at in the segment's

metadata, alongside whatever else that entry carries. The picker sorts on it: newest release first, a [1m] entry immediately after the base model it derives from, undated entries last in stored order, and pinned entries on top regardless of date. The sort is display-only -- the stored list stays append-only.

Every failure is quiet: discovery runs in the background thread, and a refusal, a timeout or a missing token leaves the picker exactly as it was.

#Staleness verification

When a slow discovery completes and returns new results, previously discovered values that disappeared from the new list are not automatically dropped. Instead, if the discovery type has a verify callback, each removed value is checked (e.g. "does the binary still exist on disk?"). Values that pass verification are kept; values that fail are dropped.

#Cross-segment constraints

An option can carry constraints that reference another segment's selection. The evaluate_requires function runs every render cycle, computing the unavailable set for each segment; unavailable options are dimmed in the UI and refuse to launch -- the cursor can still reach them, but confirming the launch with one selected is rejected with a flash.

The model segment is the one that uses this, and its constraints are not declared in options.json at all -- they are derived from claudewheel's own table of model minimum CLI versions (MODEL_MIN_CLI_VERSION in defaults.py), which is the single place that fact is written down:

  • A model listed there is dimmed whenever the effective Claude Code version is

older than its minimum, and so is the model's [1m] spelling, which inherits the base model's minimum.

  • The effective version is resolved by effective_cli_version in

binaries.py: the version segment's selection if there is one, otherwise the version the claude symlink points at, which is the binary a launch with no selection would run. Nothing dims when that is new enough, and the fallback applies just as well when the version segment is not enabled at all. Neither answer available -- no selection and no resolvable symlink -- means the version could not be determined at all, and nothing is dimmed: the minimum is enforced on a positive determination that the effective version is too old, never on an unknown one.

  • A model absent from the table is unrestricted: no selection and no installed

binary can dim it.

  • The same table drives the pre-launch model-version-guard step, which

aborts a launch whose effective binary is too old for the selected model. The step reads that table through the same effective_cli_version call the dimming makes, so the two cannot disagree about which binary a model needs or about which binary you are on: neither fact exists in two places to drift apart. They agree in every case, including the one where the version cannot be determined at all: both act only on a positive too-old determination, so a version nothing can resolve restricts nothing. The launch proceeds, and Claude Code itself reports the incompatibility if there is one.

#options.json structure

Each segment key maps to an object with:

{} json
{
  "profile": {
    "values": [],
    "pinned": ["work", "personal"],
    "discovery": {
      "type": "claude_config_scan",
      "base_dir": "~"
    }
  },
  "model": {
    "values": ["claude-opus-5", "claude-opus-4-8", "..."],
    "pinned": [],
    "metadata": {"claude-opus-5": {"created_at": "2026-02-05T00:00:00Z"}},
    "discovery": {
      "type": "anthropic_models"
    }
  },
  "directory": {
    "values": [],
    "pinned": [],
    "discovery": {
      "type": "directory_scan",
      "parents": ["~/Projects", "~/repos", "~/src"],
      "state_field": "recent_dirs"
    }
  }
}
  • values -- for every segment but model, a legacy list superseded by the

pinned/discovered/defaults split and kept for backward compatibility; migration 3 classifies existing values into the appropriate collection. For model it is the accumulated option list the picker actually offers: the models claudewheel shipped with, plus every model discovery has since reported. It is append-only -- entries are added at the end and never removed, and the built-in list in defaults.py is only the first-run seed and the source new shipped defaults are appended from. The picker reads this list and nothing else: an empty one is an empty picker, never a silent fallback to the built-in list -- the startup sync is what keeps it populated.

  • pinned -- user-added values that persist across restarts.
  • discovery -- configuration for the segment's discovery function (type plus

type-specific parameters like path, parents, count, state_field).

  • metadata -- per-value metadata dict (auth status for profiles, model_id

and the discovered created_at release date for models).

#segments.json

Each entry in the segments array defines a segment's visual and behavioral properties:

segments.json
PropertyTypeDefaultDescription
keystring--Unique identifier, matches the options.json key
labelstring--Display label in the TUI bar
show_optionsbooltrueWhether to render the fan-out option list below this segment
wrapbooltrueWhether cycling past the last option wraps to blank
min_widthint6Minimum character width for the segment
max_widthint20Maximum character width for the segment
requiredboolfalseWhether a value must be selected before launching
searchableboolfalseWhether typing filters options via fuzzy matching
tab_advancesbooltrueWhether Tab moves focus to the next segment
creatableboolfalseWhether the segment shows a + option for inline creation
freeformboolfalseWhether typed text can be submitted as a new value directly

#state.json

Runtime state persisted between sessions:

state.json
KeyDescription
last_configDict of segment key to last-selected value. Restored on next launch to pre-select previous choices.
recent_dirsList of recently used directories (capped at 20, most recent first). Used as hints by directory discovery.
launch_countTotal number of successful launches.
npm_versions_cacheCached npm version list with a fetched_at timestamp for TTL.
model_list_cacheCached Anthropic model list (id plus created_at) with a fetched_at timestamp for TTL.
auth_browserBrowser path chosen in the auth wizard (written out-of-band).
project_hook_approvalsPer-project hook approval decisions, keyed by canonical project path.
vanilla_guardrails_opt_inMachine-global opt-in state for vanilla profile guardrails.
scratchpad_snooze_untilISO-8601 deadline for snoozing the scratchpad cleanup prompt.

Some state keys are "out-of-band": they are written directly to disk by subsystems (the auth wizard, preflight steps) while the TUI holds its own in-memory copy. When the TUI saves state, it re-reads these keys from disk and lets the disk values win, preventing stale in-memory state from clobbering concurrent writes.

#Schema migration

claudewheel uses two migration mechanisms that run at startup.

#Key-additive migration (_migrate)

The first pass adds missing keys to existing config files without overwriting user values. It runs every startup and is idempotent:

  • config.json -- missing top-level keys are added from DEFAULT_CONFIG.
  • segments.json -- for each segment matched by key, missing attributes

are added from DEFAULT_SEGMENTS.

  • Theme files -- missing keys are deep-merged from the default theme dicts.
  • options.json -- new model values from DEFAULT_OPTIONS are appended to

the user's model list, and the model segment is given its discovery config when it has none (a file written before model discovery existed). The segment entry and its values list are created when absent.

Files are written only when something actually changed.

#Versioned migrations (_run_versioned_migrations)

The second pass runs one-time schema fixes keyed by _schema_version in config.json. Each migration has a version number and runs exactly once (when the config's _schema_version is less than the migration's version). After all applicable migrations run, _schema_version is bumped to the highest applied version.

The migrations themselves are not reproduced here. The _MIGRATIONS list in config.py is the authority: it holds every migration in replay order, and each entry carries its version number, a description of what it fixes, and the function that applies it. Read that list to see which fixes exist.

#Adding a new migration

  1. Write a function with the signature

(config, segments_def, theme, options_def, state) -> None that mutates in place. state is the in-memory state.json, so a migration that retires an option can repair a launch selection naming it in the same pass. Every file the migrations touch is written back only when one actually changed it.

  1. Append an entry to the _MIGRATIONS list in config.py with the next

version number.

  1. The migration runs against all theme files uniformly (not just the

terminal-resolved one), so schema fixes are deterministic regardless of which theme the user renders.

#Non-interactive overrides (the --set flag)

The launch command supports setting segment values from the command line, bypassing the TUI entirely when all required segments are covered.

#Per-segment flags

Each segment has a dedicated flag:

c --profile work --model claude-opus-4-8 --directory ~/Projects/myapp

#The -s / --set flag

The generic --set (short: -s) flag sets any segment as KEY=VALUE:

c -s profile=work -s model=claude-opus-4-8

It is repeatable but each segment can only be set once. Setting the same segment via both a dedicated flag and -s is an error:

c --profile work -s profile=personal   # error: duplicate

#TUI bypass

When all required segments have values (from flags, -s, or defaults), the TUI is skipped entirely. The directory segment defaults to the current working directory when not explicitly set.

Skipping the TUI does not make the launch non-interactive: pre-launch steps still prompt when there is a controlling terminal to prompt at. Interactivity is derived from /dev/tty being openable (and print mode being off), so a launch from a script or a daemon takes the non-interactive branch of every step rather than trying to open a terminal that does not exist.

Values from last_config in state.json fill in any segments not covered by flags, so a user who always uses the same profile and model can launch with no flags at all after the first interactive session.

#Interaction with --print-prompt

The --print-prompt member of the session selection activates non-interactive print mode. In this mode, only segments marked print_mode: true in segments.json are used. Missing required print-mode segments trigger a warning but do not block the launch.

#Resetting configuration

The reset-options command deletes options.json so it regenerates from defaults on the next run:

c reset-options

The config command opens ~/.claudewheel/ in your $EDITOR for manual editing:

c config

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • rlsbl Release orchestration and project scaffolding CLI that bumps versions, validates a structured JSONL changelog, tags only the commit CI verified, and publishes to npm, PyPI, Go and more
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search