Panel.qml keeps the state, the CLI calls and the stage switch (889 lines, was 2360); each stage is its own file taking the panel as a required property. The omarchy installer copies every QML/JS file and clears stale ones. tests/static.sh filters qmllint on [syntax], not the word error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 KiB
Architecture
How the pieces of claude-mode fit together, what each file on disk is for, and where to go to change something. Why it is shaped this way is in design.md; how to work on it, in development.md.
The pieces
flowchart LR
user([you]) --> cli["claude-mode<br/>(bash on Linux/macOS,<br/>PowerShell on Windows)"]
widget["bar widget<br/>(Omarchy, QML)"] -- runs --> cli
cli -- all JSON through --> engine["cm-json.py<br/>(POSIX only)"]
engine --> providers[(providers.json)]
engine --> presets[(presets/*.json)]
cli -- a switch writes --> settings[(~/.claude/settings.json)]
cli --> state[(state.json)]
cli --> health[(health.json)]
claude["Claude Code"] -- reads at startup --> settings
claude -- runs on a timer --> helper["key helper"]
helper --> state
helper --> vault[(vault)]
widget -- watches --> health
widget -- reads --> cache[(models-cache.json)]
- The CLI does everything that changes state. The POSIX port is bash for
control flow and terminal UI, with every piece of JSON handled by
cm-json.py— bash only ever sees flat lines of text. The Windows build is one PowerShell script that does the same withConvertFrom-Json. - A switch rewrites the managed keys in
~/.claude/settings.json(anenvblock andapiKeyHelper) and records what it wrote instate.json. Claude Code readssettings.jsonat startup, from every launch context. - The key helper is what
apiKeyHelperruns. It prints the active preset's key from the vault and nothing else, so no key ever sits insettings.json. Claude Code re-runs it on a timer, in every live session. - The bar widget never reads config itself. It watches
health.json, which the CLI rewrites on every change, and runs the CLI for anything it does. providers.jsondescribes every gateway provider as data, read by all three (the widget throughhealth.json). Behaviour that differs in kind is chosen by name from it — see providers.md.
Files on disk
Under ~/.claude-mode/:
| file | written by | read by |
|---|---|---|
bin/claude-mode, bin/lib/*.sh, bin/cm-json.py, bin/cm-vault.sh, bin/claude-key-helper.sh |
the installer | — (Windows: claude-mode.ps1 and lib\*.ps1 at the top, bin\claude-key-helper.ps1 + .cmd) |
providers.json |
the installer, every time | the CLI, the key helper's engine, the widget (via health.json) |
presets/*.json |
the installer seeds them; preset, setup, the panel edit them |
everything |
state.json |
a switch; preset rename of the active preset |
the CLI, the key helper, the widget |
defaults.json |
preset default (and rename/rm keep it right) |
the CLI; published in health.json |
health.json |
a switch, status, doctor, preset edits, health |
the widget |
models-cache.json |
every catalogue fetch | the panel's model picker |
ignored-sessions.json |
repair-session --ignore / --unignore |
the session scan |
vault/ |
set-key (file backend; DPAPI on Windows) |
the key helper |
backups/ |
a switch, before it writes settings.json (last 20 kept) |
you, if needed |
VERSION |
the installer | the CLI, stamped into health.json |
Outside it: ~/.claude/settings.json (a switch writes the managed keys and never
anything else), ~/.claude/projects/*/*.jsonl (Claude Code's transcripts, which
repair-session reads and repairs), and on Omarchy
~/.config/omarchy/plugins/smoido.claude-mode/ (the widget).
The repository
claude-mode.ps1, lib/*.ps1 the Windows build: settings + dispatch, and its modules
install.ps1, profile-snippet.ps1 its installer, and the block it adds to the PowerShell profile
bin/ the Windows key helper
linux/ the POSIX port: claude-mode (settings + dispatch),
lib/*.sh (its modules), cm-json.py, cm-vault.sh,
claude-key-helper.sh, install.sh
omarchy/ the bar widget and its installer
providers.json, presets/ shared by both builds, byte for byte
scripts/ test.sh, bump-version.sh
tests/ static, python, cli and windows suites
docs/ this
The POSIX CLI's modules
linux/claude-mode holds the paths and flags, loads these in order, and
dispatches the command line. It finds them — and cm-json.py and cm-vault.sh
— beside itself, following the ~/.local/bin symlink, so a checkout runs its own
code rather than the installed version's.
linux/lib/ |
what is in it |
|---|---|
output.sh |
the theme-derived colour palette, ok/warn/err, the banner, usage |
core.sh |
state and preset helpers, the provider table (prov_field, provider_resolve), resolve_preset |
preflight.sh |
cm_preflight, the server probe, claude-mode preflight |
sessions.sh |
finding running sessions, claude-mode sessions, and the question a switch asks about them |
switch.sh |
set_mode, settings backups, stray env vars, health.json, the guardrail check, claude-mode repair |
catalogue.sh |
provider_catalogue and the model cache |
commands.sh |
status, presets, models, set-key |
doctor.sh |
doctor and its per-provider checks |
presets.sh |
claude-mode preset … and re-applying the preset in use |
menu.sh |
the interactive menu and its pickers |
setup.sh |
claude-mode setup |
repair.sh |
claude-mode repair-session and dismissals |
The Windows script's modules
claude-mode.ps1 holds the help block, the parameters, the paths and flags and
the managed-key list, then dot-sources these from lib\ beside it, in order, and
dispatches. Inside a dot-sourced file $PSScriptRoot is lib\, so paths beside
the main script go through $script:Here.
lib/ |
what is in it |
|---|---|
providers.ps1 |
reading providers.json; Get-Provider, Resolve-ProviderId, Test-ProviderDoctor |
output.ps1 |
Write-Ok/Write-Warn2/Write-Err2, mode colours, the banner, Show-Usage |
files.ps1 |
JSON read/write (5.1 has no -AsHashtable), Protect-FileAcl |
core.ps1 |
state.json, presets, Resolve-PresetForProvider |
vault.ps1 |
the DPAPI vault, Get-PresetAuth |
switch.ps1 |
Set-ClaudeMode, settings backups, the apiKeyHelper command line |
guards.ps1 |
the cost guard, the OpenRouter guardrail check, stale cached model ids, repair |
health.ps1 |
health.json; persistent environment variables that would override a switch |
catalogue.ps1 |
Get-ProviderCatalogue, LM Studio's model list and template check, the doctor model and context checks |
commands.ps1 |
status, presets, preset, models, doctor |
menu.ps1 |
the interactive menu, its pickers, interactive preset editing, New-PresetScaffold |
Contracts between the pieces
These are the interfaces one piece relies on another to keep. Each is pinned by a test.
providers.json— its shape and the values each field may take (providers.md);tests/static.shenforces them.provider-tsv—cm-json.pyhands bash one tab-separated row per provider, and bash addresses the columns by number (bash 3.2 has no associative arrays). The column order isPROVIDER_TSVincm-json.py, mirrored in a comment inlinux/claude-mode: append only. Pinned bytests/python.- The managed env keys — the keys a switch owns and clears:
BASE_MANAGEDincm-json.pyand$script:BaseManagedEnvKeysinclaude-mode.ps1must list the same keys. Pinned bytests/python. health.json— what the widget reads:mode,preset,version,models,contextTokens,keyBackend,presets(the catalogue),providers(id, title, blurb, logo, logoScale, serverEditable, perServerCatalogue, defaultKeyRef, defaultBaseUrl, serverHint, keyOptional),defaultPresetFor,defaultPresetChosen. No key material, ever.claude-mode preflight <mode> [preset]— JSON the widget acts on:ok,code(ok,needs-setup,no-models,no-url,missing-key,helper-missing,server-auth,server-wrong,server-unreachable,no-preset,provider-mismatch),title,detail,remedy(a command), andremedyKind, which picks the panel's button (setup,set-key,edit-preset,set-url,needs-key,start-server).claude-mode repair-session --json— the scan:broken[]andignored[](each withsessionId,project,path,dropLines,providers,mtime, andreasonfor the ignored),count,ignoredCount,maxAgeDays.models-cache.json—providers.<id>→fetchedAt,ok,failedAt,baseUrl(per-server providers),models[](id, andcontextTokens,priceIn,priceOut,state,notewhere known). No keys.- The key helper — prints the key and nothing else on stdout; every failure
explains itself on stderr, because Claude Code shows that under
/status. It reads the preset in one open and re-readsstate.jsononce on a miss, so a rename of the active preset cannot catch it mid-lookup.
Where to change what
| to change | POSIX | Windows |
|---|---|---|
| a provider's endpoint, auth template, setup, checks or look | providers.json |
the same file |
| a shipped preset | presets/*.json |
the same files |
what a switch writes to settings.json |
cm-json.py cmd_apply, BASE_MANAGED |
lib/switch.ps1 Set-ClaudeMode; $script:BaseManagedEnvKeys in claude-mode.ps1 |
| a command's arguments | the dispatch at the end of linux/claude-mode |
the dispatch at the end of claude-mode.ps1 |
| what refuses a switch | linux/lib/preflight.sh cm_preflight |
the guards in Set-ClaudeMode |
| how a model list is fetched | linux/lib/catalogue.sh provider_catalogue + a parser in cm-json.py |
lib/catalogue.ps1 Get-ProviderCatalogue |
| a doctor check | linux/lib/doctor.sh |
lib/commands.ps1 Invoke-Doctor, lib/catalogue.ps1 |
| setup | linux/lib/setup.sh |
— |
| session detection | linux/lib/sessions.sh |
— |
| session repair | linux/lib/repair.sh; cm-json.py cmd_scan_sessions, cmd_repair_session |
— |
| key storage | linux/cm-vault.sh |
lib/vault.ps1 |
| the bar icon and state | omarchy/smoido.claude-mode/BarWidget.qml |
— |
| what the panel does | Panel.qml (state, actions, the processes it runs) |
— |
| how one panel stage looks | its own file, below | — |
The bar widget's files
All in omarchy/smoido.claude-mode/. The panel owns every piece of state and
every action; each stage file only draws, from the panel it is handed.
| file | what is in it |
|---|---|
BarWidget.qml |
the icon, the state it watches (health.json, state.json, the model cache), the session scan, the provider lookup |
Panel.qml |
the panel's state and actions, the CLI processes they run, the hero and the footer, and the stages in stacking order |
ModeList.qml |
the modes to switch to, each unfolding its presets |
BlockedStage.qml |
a refused switch, with the fix |
ServerStage.qml |
a server provider's address and key |
PresetStage.qml |
the preset editor |
TierStage.qml |
picking one tier's model |
NewPresetStage.qml, RenameStage.qml, DeleteStage.qml |
a preset's lifecycle |
SessionsStage.qml |
the running-sessions question before a switch |
BrokenSessions.qml, RepairStage.qml |
sessions a switch broke, and repairing one |
ModelMap.qml |
the active tier-to-model map |
PillButton.qml, BrandIcon.qml |
the panel's button, and a provider's mark |
Modes.js |
fallback presentation for anthropic and an older health.json |
A stage cannot reach into another file by id, so where an action has to touch a
stage's field — clearing the model search, focusing a name field — Panel.qml
emits a signal and the stage's Connections block does it.