# 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](design.md); how to work on it, in [development.md](development.md). ## The pieces ```mermaid flowchart LR user([you]) --> cli["claude-mode
(bash on Linux/macOS,
PowerShell on Windows)"] widget["bar widget
(Omarchy, QML)"] -- runs --> cli cli -- all JSON through --> engine["cm-json.py
(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 with `ConvertFrom-Json`. - **A switch** rewrites the managed keys in `~/.claude/settings.json` (an `env` block and `apiKeyHelper`) and records what it wrote in `state.json`. Claude Code reads `settings.json` at startup, from every launch context. - **The key helper** is what `apiKeyHelper` runs. It prints the active preset's key from the vault and nothing else, so no key ever sits in `settings.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.json`** describes every gateway provider as data, read by all three (the widget through `health.json`). Behaviour that differs in kind is chosen by name from it — see [providers.md](providers.md#adding-a-provider). ## 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](providers.md#adding-a-provider)); `tests/static.sh` enforces them. - **`provider-tsv`** — `cm-json.py` hands bash one tab-separated row per provider, and bash addresses the columns by number (bash 3.2 has no associative arrays). The column order is `PROVIDER_TSV` in `cm-json.py`, mirrored in a comment in `linux/claude-mode`: append only. Pinned by `tests/python`. - **The managed env keys** — the keys a switch owns and clears: `BASE_MANAGED` in `cm-json.py` and `$script:BaseManagedEnvKeys` in `claude-mode.ps1` must list the same keys. Pinned by `tests/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 [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), and `remedyKind`, 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[]` and `ignored[]` (each with `sessionId`, `project`, `path`, `dropLines`, `providers`, `mtime`, and `reason` for the ignored), `count`, `ignoredCount`, `maxAgeDays`. - **`models-cache.json`** — `providers.` → `fetchedAt`, `ok`, `failedAt`, `baseUrl` (per-server providers), `models[]` (`id`, and `contextTokens`, `priceIn`, `priceOut`, `state`, `note` where 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-reads `state.json` once 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` | — | | the panel | `omarchy/smoido.claude-mode/Panel.qml` | — |