Files
claude-mode/README.md
T
smoido 9c301e10a1 Correct what a switch actually does to a running session
Every one of these said a switch leaves running sessions alone - that they keep
talking to the old provider until restarted. That is wrong, and reassuring in
exactly the wrong direction: a switch breaks them.

The static half of the config does behave as described. Base URL, model ids and
the env block are read once at startup and a running session keeps what it
started with.

The credential is not. It comes from running apiKeyHelper, which Claude Code
re-invokes on a timer - CLAUDE_CODE_API_KEY_HELPER_TTL_MS is present in the
2.1.251 binary - and claude-key-helper.sh answers for whatever state.json says at
that moment. So the switch reaches into a live session through the one part that
was never cached. Into anthropic, the helper returns nothing by design and the
next refresh comes back with no credential; into another provider, it hands over
the new key while the session is still pointed at the old base URL, which
rejects it. Either way the session starts failing calls whenever the TTL happens
to expire, mid-turn as easily as between turns.

One case does survive, and is now stated rather than glossed: switching between
two presets of the same provider that share a keyRef keeps the same key and the
same endpoint, so the session carries on with the model ids it started with.

The claim in "Restarting sessions" predates this work - it came in with the
Windows build - but it is wrong for the same reason and is corrected too.
2026-08-30 21:41:09 +03:00

610 lines
28 KiB
Markdown

# claude-mode
Switch Claude Code system-wide between **Anthropic**, **OpenRouter**, **Z.AI**, and a
local **LM Studio** server — with named per-tier model presets.
Windows / PowerShell 5.1 with no external dependencies, and a POSIX port for
Linux and macOS that needs only python3 — including a theme-aware TUI and an
Omarchy bar widget. See [Linux / Omarchy](#linux--omarchy).
```
claude-mode # interactive menu
claude-mode anthropic # subscription login
claude-mode openrouter # remote gateway (preset: default)
claude-mode zai # Z.AI GLM coding plan (preset: zai)
claude-mode lmstudio # local server (preset: lmstudio)
```
Works for the CLI, the VS Code extension, and the desktop app from a single
switch. Restart Claude Code afterwards — nothing else.
---
## The interactive menu
Run `claude-mode` with no arguments. The mode you're already in is omitted —
there's nothing to switch to:
```
claude-mode (currently: openrouter / default)
switch mode:
1) Anthropic - your subscription login, no gateway
2) Z.AI - GLM coding plan
3) LM Studio - local server, offline, free
4) show full status
5) edit presets
6) run doctor
0) quit
```
Pick a provider and it lists that provider's presets with the default marked;
**Enter** accepts it. Option 5 walks preset → tier → new model ID, and if you
edit the preset that's currently live it re-applies immediately.
When stdin is redirected (scripts, CI) the menu is skipped and `status` prints
instead, so `claude-mode` is still safe in a pipeline.
## Why settings.json and not a profile export
Three ways to make this persist. I picked the second.
**1. Export the variables from `$PROFILE`.**
The obvious move, and the wrong one here. It only covers processes launched from
a PowerShell session that loaded the profile — which is exactly *not* how you use
Claude Code. The VS Code extension is spawned by VS Code, not by your shell, so
it would never see the exports. Same for the desktop app, `cmd.exe`, and any
terminal opened before the switch. Worse, the failure is silent: you switch to
`anthropic`, a shell opened five minutes ago still has `ANTHROPIC_BASE_URL` set,
and that session quietly keeps billing OpenRouter.
**2. Rewrite the `env` block in `~/.claude/settings.json`.** ← chosen
Claude Code reads this file on every startup, from every launch context. One
write, and the next `claude` — CLI, extension, desktop — picks it up. A switch is
atomic: one file, one source of truth. `claude-mode anthropic` *deletes* the
managed keys rather than blanking them, so nothing can linger and break native
auth. The cost is that config is global rather than per-terminal.
**3. Persistent User-scope environment variables (`setx`).**
Also global and reboot-proof, but strictly worse: new processes only, values sit
in the registry in plaintext, and a stale entry silently outranks whatever
`claude-mode` writes. This tool treats them as a fault condition — `status` and
`doctor` flag them and offer removal, backing the old value up first.
The `claude` wrapper in the profile is a **safety net, not the mechanism**. It
strips inherited process-level copies of all thirteen managed variables before
launching `claude.exe`. Everything still works without it — including in VS Code,
which never loads the profile.
## Why API keys are not in settings.json
`settings.json` is a config file you'll hand-edit, diff, and possibly paste into
a bug report. A `sk-or-` or Z.AI token does not belong there.
Keys are stored **DPAPI-encrypted** in `~/.claude-mode/vault/*.cred` — encrypted
against your Windows account on this machine, so copying the file elsewhere or
reading it as another user yields nothing — with the file ACL restricted to you.
Claude Code receives the key at runtime through `apiKeyHelper`, which decrypts
and prints it. `settings.json` holds only the base URL and model IDs.
In `anthropic` mode the helper is removed from settings.json *and* returns
nothing when state says `anthropic` — belt and braces. LM Studio's `lmstudio`
token is a placeholder, not a secret, so it's written inline and the helper stays
out of it.
## Commands
```
claude-mode interactive menu
claude-mode status active mode, preset, model map
claude-mode anthropic native login (deletes all managed keys)
claude-mode openrouter [preset] default preset: default
claude-mode zai [preset] default preset: zai (alias: z.ai, z-ai)
claude-mode lmstudio [preset] default preset: lmstudio
claude-mode presets list presets (* = active)
claude-mode preset show <name>
claude-mode preset new <name> [from] copy an existing preset
claude-mode preset set <name> <tier> <model-id>
claude-mode preset all <name> <model-id> point every tier at one model
claude-mode preset rm <name>
claude-mode set-key [ref] store a key (hidden prompt, DPAPI)
claude-mode models [filter] models available from the active provider
claude-mode doctor verify auth, endpoint, model ids, stray env vars
claude-mode preflight <mode> [preset] can this mode actually serve? (no switch)
claude-mode sessions running sessions, and which are mid-request
claude-mode sessions --stop close them (asks first)
claude-mode sessions --restart close and reopen each in its own directory
claude-mode sessions --dry-run show what either would do, and do nothing
claude-mode <mode> --force switch even when preflight says no
```
Omitting the preset uses a **fixed** per-provider default, not "most recently
used" — so `claude-mode openrouter` always means `default`.
## Presets shipped
| preset | provider | opus | sonnet | haiku | fable |
|---|---|---|---|---|---|
| `default` | openrouter | `deepseek/deepseek-v4-flash-0731` | `qwen/qwen3.7-flash` | `openrouter/free` | `z-ai/glm-5.3` |
| `cheap` | openrouter | `deepseek/deepseek-v4-pro` | `deepseek/deepseek-v4-flash-0731` | `qwen/qwen3.7-flash` | `openai/gpt-5.6-luna-pro` |
| `zai` | zai | `glm-5.3` | `glm-5.3` | `glm-4.7` | `glm-5.3` |
| `lmstudio` | lmstudio | `kwaipilot_kat-coder-v2.5-dev` (all tiers) | | | |
| `lmstudio-qwen` | lmstudio | `qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved` (all tiers) | | | |
Presets are plain JSON in `~/.claude-mode/presets/`. A preset declares its
`provider`; `claude-mode lmstudio default` is rejected rather than silently
pointing a local URL at remote model IDs.
To route the real Anthropic models through OpenRouter, copy one and repoint the
tiers - `claude-mode preset new claude-via-or default`, then `preset set` each
tier to `anthropic/claude-opus-5` and friends.
## Context windows and early auto-compaction
**Symptom:** switch to a gateway and the session starts auto-compacting almost
immediately, even though every model involved has a huge context window.
**Cause:** behind a custom `ANTHROPIC_BASE_URL`, Claude Code has no way to
resolve a third-party model ID like `deepseek/deepseek-v4-flash` to a context
length. It falls back to a conservative default and starts compacting against
*that*, not against the model's real 1M window. Z.AI's own docs work around this
by setting `CLAUDE_CODE_AUTO_COMPACT_WINDOW=1000000` — they hit the same thing.
**Fix:** every preset carries a `contextTokens` field, which writes both knobs
(confirmed present in CLI 2.1.221):
```
CLAUDE_CODE_MAX_CONTEXT_TOKENS = <contextTokens>
CLAUDE_CODE_AUTO_COMPACT_WINDOW = <contextTokens>
```
| preset | contextTokens |
|---|---|
| `default`, `cheap`, `zai` | 1,000,000 |
| `lmstudio`, `lmstudio-qwen` | 262,144 |
`doctor` cross-checks the declared window against each tier's *actual* model
window and names any tier that falls short — `default` maps haiku to
`openrouter/free` (200k), which it flags as harmless since haiku only runs short
background tasks. Switching without `contextTokens` prints a warning.
Adjust per preset:
```powershell
# edit ~/.claude-mode/presets/<name>.json -> "contextTokens": 262144
claude-mode doctor # re-checks declared vs actual
```
## Z.AI mode
Replaces `npx @z_ai/coding-helper` — and does something it doesn't: **maps a
distinct model to each Anthropic tier** instead of forcing one model everywhere.
Per [Z.AI's Claude Code docs](https://docs.z.ai/devpack/tool/claude):
| setting | value |
|---|---|
| `ANTHROPIC_BASE_URL` | `https://api.z.ai/api/anthropic` |
| auth | your Z.AI API key — kept in the DPAPI vault, delivered via `apiKeyHelper` |
| `API_TIMEOUT_MS` | `3000000` |
| `CLAUDE_CODE_AUTO_COMPACT_WINDOW` | `1000000` |
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | `1` |
All three extra variables are confirmed present in CLI 2.1.221. Setup:
```powershell
claude-mode set-key zai # paste your key from https://z.ai/manage-apikey/apikey-list
claude-mode zai
claude-mode doctor # sends a 1-token request to prove the key works
```
## LM Studio mode
Per [LM Studio's docs](https://lmstudio.ai/docs/integrations/claude-code): base
URL `http://127.0.0.1:1234` (**base only** — Claude Code appends `/v1/messages`),
token `lmstudio`, plus `CLAUDE_CODE_ATTRIBUTION_HEADER=0`. Gateway discovery
stays off; the Anthropic surface is `/v1/messages` only.
### Model IDs are not what the UI shows
LM Studio's `/v1/models` lists only **loaded** instances under their display
alias. `kat-coder-v2.5` is such an alias — once the model idle-unloads it
vanishes, and a request using that name returns `400 No models loaded`. The
JIT-loadable ID is the model key, `kwaipilot_kat-coder-v2.5-dev`. `claude-mode`
reads `/api/v0/models` instead, which lists every installed model with its load
state, so `models` and `doctor` show IDs that actually work.
### The `[Server Error] ... Unable to generate parser for this template` spam
Cause: some GGUF chat templates hard-assert message ordering —
```jinja
{%- if message.role == "system" %}
{%- if not loop.first %}
{{- raise_exception('System message must be at the beginning.') }}
```
Runtimes that auto-generate a tool-call parser probe the template with synthetic
message sequences; those probes trip the assertion and the request dies. It's a
model-template bug, not a Claude Code or claude-mode bug — it's been reported
against several models
([LM Studio #1999](https://github.com/lmstudio-ai/lmstudio-bug-tracker/issues/1999),
[llama.cpp #20733](https://github.com/ggml-org/llama.cpp/issues/20733)).
Scanning your installed models' templates:
| model | template |
|---|---|
| `qwen3.6-35b-a3b-uncensored-heretic-native-mtp-preserved` | clean |
| `qwen3.6-35b-a3b` | clean |
| `qwen2.5-coder-7b-instruct`, `google/gemma-4-12b-qat` | clean |
| **`kwaipilot_kat-coder-v2.5-dev`** | **asserts** |
| **`qwen/qwen3.5-9b`**, **`prism-ml/bonsai-27b`** | **assert** |
`doctor` now reports this per model, and `models` flags affected entries with
`TEMPLATE RISK`. **The fix is to use a model without the flag** — which is
exactly the `lmstudio-qwen` preset, verified end-to-end with a cold JIT load,
streaming, and tool calls.
Honest caveat: KAT-Coder's template *does* contain the assertion, but I could not
reproduce the failure against it here — cold JIT, streaming, tools, system
blocks, and multi-turn `tool_result` all succeeded. Whether it trips seems to
depend on which parser strategy the runtime picks. If it spams, switch:
```powershell
claude-mode lmstudio lmstudio-qwen
```
Other notes: use a model with **>25k context** (`doctor` warns below that), and
the model must be **installed** — JIT loading handles "not loaded" fine.
If you enable authentication in LM Studio, move that preset to a vault key:
```powershell
claude-mode set-key lmstudio
# then in ~/.claude-mode/presets/lmstudio.json:
# "auth": { "mode": "vault", "keyRef": "lmstudio" }
```
## CLI version
Verified against `claude.exe` **2.1.221** by scanning the binary — every variable
this tool writes is referenced by it:
`ANTHROPIC_BASE_URL` · `ANTHROPIC_AUTH_TOKEN` · `ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL` ·
`CLAUDE_CODE_SUBAGENT_MODEL` · `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` ·
`CLAUDE_CODE_ATTRIBUTION_HEADER` · `CLAUDE_CODE_AUTO_COMPACT_WINDOW` ·
`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` · `API_TIMEOUT_MS` · `apiKeyHelper`
(On 2.1.89 the fable and gateway-discovery vars did not exist; the update to
2.1.221 added both.)
## Setup
```powershell
cd c:\Users\smoido\Projects\cli\claude-code-switcher
.\install.ps1
```
Installs to `~/.claude-mode/` (ACL: you only), drops `claude-mode.cmd` into
`~/.local/bin` (already on your User PATH, next to `claude.exe`), and adds a
marked block to `~/Documents/WindowsPowerShell/profile.ps1`.
`~/.claude/settings.json` is **not** touched by the installer — only by an actual
mode switch, which backs it up to `~/.claude-mode/backups/` first (last 20 kept).
If the profile doesn't load: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`.
## Restarting sessions
A switch **breaks** running sessions. This originally said it did not affect
them; that was wrong, and the difference matters, because the two halves of the
config behave differently.
The static half — base URL, model ids, the env block — genuinely is read once at
startup, and a running session keeps what it started with.
The credential is not. It comes from running `apiKeyHelper`, which Claude Code
re-invokes on a timer (`CLAUDE_CODE_API_KEY_HELPER_TTL_MS`, present in 2.1.251),
and the helper answers for whatever `state.json` says *at that moment*. So a
switch reaches into a live session through the one thing that was never cached:
| switching to | what the running session gets |
|---|---|
| `anthropic` | the helper returns nothing, by design — no credential at all |
| another provider | the new provider's key, sent to the old base URL, which rejects it |
| another preset of the *same* provider, same `keyRef` | same key, same endpoint — this one survives, on the model ids it started with |
Either of the first two starts failing calls whenever the TTL happens to expire:
mid-turn as easily as between turns. So restart afterwards — and on Linux,
`claude-mode sessions --restart` will do it for you:
- **CLI** — exit and relaunch `claude`
- **VS Code** — `Ctrl+Shift+P`*Developer: Reload Window*
- **Desktop app** — quit and reopen
`claude-mode status` shows what the *next* launch will use.
## Layout
```
~/.claude-mode/
claude-mode.ps1 main script
state.json mode, active preset, and the exact env keys last written
presets/*.json provider + model maps
vault/*.cred DPAPI-encrypted keys (openrouter, zai, ...)
backups/ settings.json snapshots + removed env-var values
bin/claude-key-helper.cmd apiKeyHelper shim
~/.local/bin/claude-mode.cmd PATH entry point (works from any shell)
~/Documents/WindowsPowerShell/profile.ps1
claude-mode + claude functions, between markers
```
`state.json` records which env keys the last switch actually wrote, so a custom
`extraEnv` key (Z.AI's timeouts, LM Studio's attribution header) is removed when
you switch away — even though no other preset knows that key exists.
## Linux / Omarchy
The `linux/` tree is a POSIX port of the same design: one write to
`~/.claude/settings.json`, secrets kept out of it, presets shared verbatim with
the Windows build.
```bash
cd claude-code-switcher
bash linux/install.sh # --skip-key-prompt to install without storing a key
```
Installs to `~/.claude-mode/`, symlinks `~/.local/bin/claude-mode`, and adds the
`claude` wrapper to `~/.bashrc` / `~/.zshrc` between markers.
Secrets have no DPAPI equivalent here, so the vault picks the best backend
available and says which one it chose: macOS Keychain, libsecret, `pass`, or a
0600 file that is honestly labelled as unencrypted.
### The menu follows your desktop theme
The sixteen ANSI colour slots carry no guarantee about relative brightness, and
monochrome themes exploit that. Under Omarchy's Solitude, slot 36 — headings —
resolves to `#707070` and slot 31 — `FAIL` — to `#565d60`. Against `#cacccc`
body text on a `#101315` ground that is 3.8:1 and 2.8:1 where the body text is
11.6:1, so headings render as fine print and an error becomes the quietest
thing on screen. Exactly backwards.
So when Omarchy is present, the palette is derived from the theme it publishes
at `~/.local/state/omarchy/current/theme/colors.toml` instead. Every role is
measured against the background it will actually be drawn on and lifted toward
the foreground when it falls short, which keeps hue where the theme has any and
falls back to weight where it does not:
| role | before (Solitude) | after |
|---|---|---|
| heading | 3.8:1 | 9.4:1 |
| `FAIL` | 2.8:1 | 5.2:1 |
| help text | 2.2:1 | 2.2:1 (recessive on purpose, floored) |
Light themes are handled by the same arithmetic — `mode` in `colors.toml` is
authoritative, so `catppuccin-latte` and `flexoki-light` keep their accents
rather than washing out.
Overrides: `CLAUDE_MODE_THEME=/path/to/colors.toml` points it elsewhere,
`NO_COLOR` turns it off. Without Omarchy, or on a terminal that cannot do
truecolor, it falls back to the ANSI slots with the two roles the slots get
wrong corrected — bright red for `FAIL`, bold on headings.
### `claude-mode health`
Rewrites `~/.claude-mode/health.json`, the machine-readable mirror of the
active configuration: mode, preset, model map, context window, key backend, and
the switchable preset catalogue. No key material. A switch and a `status` both
refresh it; the command exists for anything that wants to force it.
## Refusing a switch that would not work
A switch writes settings.json and is picked up by the *next* `claude` launch, so
switching into a mode that cannot serve requests does not fail loudly — it
succeeds, and every session started afterwards is broken in a way that points at
Claude Code rather than at here.
LM Studio is the sharp case. Its token is an inline placeholder, so nothing
about the switch needs the server to exist; point at a server that is not
running and you get a config that looks perfectly healthy and answers nothing.
So the preconditions are checked before the write, not after:
| mode | checked | when it fails |
|---|---|---|
| `anthropic` | nothing to check | — |
| `openrouter`, `zai` | a key exists for the preset's `keyRef`, and the helper is executable | `claude-mode set-key <ref>` |
| `lmstudio` (anywhere) | the server answers, and accepts the credential the preset would send | see below |
| any preset on a local base URL | same probe | start the server |
| all | the preset exists and declares the provider being switched to | — |
The probe distinguishes four outcomes, because their remedies are opposites:
| result | means | remedy offered |
|---|---|---|
| `ok` | answered `/api/v0/models` or `/v1/models` | — |
| `auth` | the server is up and refused the credential | store or fix the key, or turn auth on for the preset |
| `notfound` | something is listening, but the API is not at that path | fix the base URL |
| `refused` | nothing answered at all — down, asleep, DNS, TLS, timeout | start the server, or fix the address |
`claude-mode preflight <mode> [preset]` runs exactly these and prints the
verdict as JSON without switching; it is what the bar widget calls before it
offers to do anything. `--force` overrides the lot.
Remote *gateways* are not probed — OpenRouter or Z.AI being briefly unreachable
is the network's problem and not worth blocking a config change over, where a
missing key never fixes itself. LM Studio is probed wherever it lives, because
an instance on a sleeping LAN box is exactly as absent as a loopback port with
nothing behind it, and produces the identical silent breakage. Off-machine
addresses get a longer timeout, not a pass.
### LM Studio somewhere other than this machine
It ships on loopback, but that is a default, not a constraint. A preset is just
a base URL and an auth block, so all of these are the same two fields:
```bash
claude-mode preset url lmstudio http://192.168.1.40:1234 # another box on the LAN
claude-mode preset url lmstudio https://lms.example.net # through a tunnel or proxy
claude-mode preset auth lmstudio key lmstudio # that server wants an API key
claude-mode set-key lmstudio # store it (vault, not settings.json)
claude-mode preset auth lmstudio none # back to the open-server default
```
`auth none` writes LM Studio's inline placeholder token, which is not a secret
and is what an unauthenticated server expects. `auth key` moves it to the vault
like every other credential — a real key on a public address is a real key.
Nothing here changes the shipped presets unless you ask it to; the local default
stays exactly as it was.
## Sessions still on the old provider
A switch does not leave running sessions on the old provider — it breaks them.
See [Restarting sessions](#restarting-sessions) for the mechanism: their
endpoint is fixed at startup but their credential is re-fetched on a timer, so
it switches under them and the endpoint they are still pointed at refuses it.
Which is why this is a decision rather than a notification.
```
claude-mode sessions
running sessions (2)
562250 pts/4 /home/smoido/Work working (this session - never touched)
631644 pts/1 /home/smoido/Projects/api
```
Sessions are found through `/proc/<pid>/exe`, which on Linux resolves to the
real `claude` binary — a process-name match would sweep up every shell that
merely mentions claude on its command line, including the one this is running
from. Two things are then filtered out:
- **The calling session.** Killing the session that asked for the kill is not a
thing anyone means, so it is listed and never signalled.
- **Forks of a session.** A busy session spawns children off its own binary,
and they inherit the same `exe`. Without excluding anything whose parent is
itself claude, the count climbs and falls with how hard the machine is
thinking — it read 2, 5, 11 and 40 on the same two sessions before this. A
real session's parent is a terminal.
`working` is a sampled-CPU heuristic — two reads of `utime + stime` 300ms apart
— so it is a good guess about which session is mid-turn, not a promise.
`--stop` sends SIGTERM (never SIGKILL; Claude Code writes out its transcript on
the way down). `--restart` stops each session and reopens it — re-running the
parent terminal's own command line where there is one, so the same terminal,
flags and directory come back, and falling back to a fresh terminal in the
session's directory otherwise. Interactive runs confirm first; `--yes` is for
callers that have already asked, and `--dry-run` prints the plan and touches
nothing.
## The Omarchy bar widget
An icon in the Omarchy top bar showing which provider the next `claude` launch
will use, and a panel that switches it without a terminal.
```bash
bash omarchy/install.sh
```
Copies the plugin to `~/.config/omarchy/plugins/smoido.claude-mode/` and adds
its id to the bar layout in `~/.config/omarchy/shell.json` (backed up first).
Both hot-reload, so nothing needs restarting — except after a change to
`Modes.js`, which the QML engine caches for the life of the process as a
`.pragma library` (`omarchy restart shell`).
The icon is the mode, and it is the provider's own logo: the Claude burst, the
OpenRouter arrow, the Z.AI Z, the LM Studio mark. They are drawn as vector paths
with `QtQuick.Shapes` rather than set as font glyphs — three of the four have no
Nerd Font pictograph at all, and being paths means they take the bar's
foreground colour and follow the theme like everything else. Anything other than
Anthropic takes the theme accent, so the bar stays quiet exactly when nothing
unusual is configured.
Marks come from [simple-icons](https://simpleicons.org) (Claude, OpenRouter, LM
Studio) and [lobe-icons](https://github.com/lobehub/lobe-icons) (Z.AI);
trademarks belong to their owners. Each carries an optical scale factor, because
equal nominal size is not equal apparent size — LM Studio's filled container
covers 69% of its box against ~38% for the other three, and Z.AI and OpenRouter
are wide-but-short marks whose ink spans only ~84% of the box height. The
factors in `Modes.js` are the geometric mean of both corrections.
Three details keep them from looking ragged in a 13px slot, which is what every
stock glyph in this bar measures:
- **No layer.** A layer rasterises the Shape at its own size and then scales the
*texture*, so a 24px buffer minified to 13 resamples ~2 pixels into 1. Without
one the scale is a transform on the geometry and rasterisation happens once,
at final resolution.
- **`Shape.CurveRenderer`** (Qt 6.6+) rasterises curves analytically instead of
tessellating them into antialiased triangles. Measured against a cairo render
of the same mark at the same size, the two now come out identical.
- **An odd `iconSize`.** These marks are radially symmetric, so their vertical
and horizontal arms sit on the centre line — which is a pixel *centre* at an
odd size and the seam between two pixels at an even one, where each arm splits
its coverage and greys out. Even values are rounded up.
- **Click** the icon for the panel: current mode, every mode with the active one
ticked, and the model map behind a gateway. Picking a gateway unfolds its
presets rather than switching blind; picking a preset starts the switch.
Choosing a target does not switch immediately — it runs the same two checks the
CLI does, and both can stop it:
1. **Preflight.** If the mode has no key stored, or its server is not answering,
the panel says which and offers the fix: *Store the key…* opens a terminal
for the hidden prompt (a bar popup has nowhere to host one), *Check again*
re-runs the preflight once the server is up, and *Server settings…* opens the
form below. The gear on any LM Studio preset row opens the same form without
waiting for a failure — per row, because two LM Studio presets can point at
two different machines.
The form holds the base URL, a *Use local default* reset, and a switch for
whether that server needs an API key. Saving rewrites the preset and drops
straight back into the switch that was blocked.
2. **Running sessions.** If any are running, the panel lists them by terminal
and directory, marks any that are mid-request, and asks: *Switch and restart*,
*Switch and close*, *Switch only*, or *Cancel*. The session action is applied
strictly **after** the write — restarting first would only bring them back up
on the provider you just left.
- **Right-click** switches straight back to Anthropic.
- **Middle-click** re-reads state.
- **Hover** for mode, preset, and the opus/sonnet mapping.
State comes from watching `health.json`, not from polling the CLI, so the
widget costs nothing while idle and a switch made in a terminal shows up in the
bar on its own. A failed switch — a preset whose key was never stored is the
common one — surfaces the CLI's own error in the panel rather than looking
like a click that did nothing.
Per-instance settings in the `shell.json` layout entry:
| key | default | meaning |
|---|---|---|
| `showLabel` | `false` | show the preset name beside the icon as well |
| `iconSize` | `13` | mark size in px; rounded up to odd (see below) |
| `root` | `~/.claude-mode` | where claude-mode is installed |
Placement is `right`, before `omarchy.agents`; override with `CM_BAR_SECTION`
and `CM_BAR_BEFORE` when installing. Moving it later is a normal
`omarchy bar move smoido.claude-mode --section <section>`.
Uninstall:
```bash
rm -rf ~/.config/omarchy/plugins/smoido.claude-mode
# then remove the {"id": "smoido.claude-mode"} entry from ~/.config/omarchy/shell.json
```
## Uninstall
```powershell
claude-mode anthropic # clean settings.json first
Remove-Item ~\.claude-mode -Recurse -Force
Remove-Item ~\.local\bin\claude-mode.cmd
# then delete the block between the >>> claude-mode >>> markers in profile.ps1
```