linux/claude-mode was 3,035 lines. It is now 177: the paths and flags, a loader, and the command dispatch. Everything else moved, verbatim, into twelve files under linux/lib/, one per concern - output, core, preflight, sessions, switch, catalogue, commands, doctor, presets, menu, setup, repair. The move was done by line range with a check that every original line landed in exactly one file; the only thing that changed place is the switch's running-sessions question, which now sits with session detection. The CLI finds lib/, 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 (it used to mix the two). The key helper path written into settings.json is still the installed one. linux/install.sh ships bin/lib/, clearing old modules first so a removed one cannot linger. The package build copies linux/ recursively, which a flat copy would not. tests/cli/test_install.sh runs the real installer into a sandbox home: every file lands, the symlink runs, a reinstall keeps edited presets and drops a stale module. docs/architecture.md lists the modules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.9 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 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, install.ps1 the Windows build and its installer
bin/ the Windows key helper
linux/ the POSIX port: claude-mode (settings + dispatch),
lib/*.sh (the rest, one file per concern), cm-json.py,
cm-vault.sh, claude-key-helper.sh, install.sh, bootstrap.sh
omarchy/ the bar widget and its installer
providers.json, presets/ shared by both builds, byte for byte
scripts/ test.sh, bump-version.sh, build-package.ps1
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 |
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 |
Set-ClaudeMode, $script:BaseManagedEnvKeys |
| 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 | lib/preflight.sh cm_preflight |
Set-ClaudeMode guards |
| how a model list is fetched | lib/catalogue.sh provider_catalogue + a parser in cm-json.py |
Get-ProviderCatalogue |
| a doctor check | lib/doctor.sh |
Invoke-Doctor, Test-PresetCatalogue, Test-OllamaContext |
| setup | lib/setup.sh |
— |
| session detection | lib/sessions.sh |
— |
| session repair | lib/repair.sh; cm-json.py cmd_scan_sessions, cmd_repair_session |
— |
| key storage | cm-vault.sh |
the DPAPI functions in claude-mode.ps1 |
| the bar icon and state | omarchy/smoido.claude-mode/BarWidget.qml |
— |
| the panel | omarchy/smoido.claude-mode/Panel.qml |
— |