Every gateway provider is now an entry in providers.json - endpoint and auth template, how its model list is read, how it is probed before a switch, what setup asks, which doctor checks apply, and its title, colour and logo - installed next to the presets and read by all three consumers: the bash CLI (through cm-json.py), the Windows script, and the bar widget (through health.json). anthropic stays built in; it is the native login, not a gateway. Behaviour that differs in kind stays in code, chosen by name from the entry: catalogue parsers (openrouter, lmstudio, ollama, openai, static), probe rules (always, lenient, local), and named doctor checks. A provider that reuses them is an entry and a default preset, with no code. The widget draws providers from health.json, so a new one needs no QML change and no shell restart. The existing three are unchanged in behaviour: their blank presets come out byte-identical from the file, and setup, doctor, models and the picker run the same checks through the generic paths. Ollama: local server on :11434, placeholder token, one model for every tier, models from /api/tags. doctor reads the context each loaded model actually runs with (/api/ps) and its maximum (/api/show), because Ollama defaults to 4096 tokens unless OLLAMA_CONTEXT_LENGTH is set and silently truncates past it. A bare model name matches its :latest tag. Custom: any Anthropic-compatible endpoint. Ships with no address and is refused until it has one; key optional; models from /v1/models when the endpoint has a list, and a lenient probe so a proxy without one is not blocked. Also: preflight (and Set-ClaudeMode on Windows) refuses a preset with no server address; the server form, setup and set-auth use each provider's own default URL, key name and placeholder token instead of LM Studio's; the Windows build gains the no-models and no-address guards it never had. Tested on Linux against fake Ollama/Custom servers, and on Windows 5.1 in a USERPROFILE sandbox on winbox. 1.12.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1011 lines
47 KiB
Markdown
1011 lines
47 KiB
Markdown
# claude-mode
|
||
|
||
Switch Claude Code system-wide between **Anthropic**, **OpenRouter**, **Z.AI**, a
|
||
local **LM Studio** or **Ollama** server, and any **custom** Anthropic-compatible
|
||
endpoint — with named per-tier model presets.
|
||
|
||
One write to `~/.claude/settings.json` covers the CLI, the VS Code extension, and
|
||
the desktop app. Restart Claude Code afterwards — nothing else. Windows
|
||
(PowerShell 5.1, no external dependencies) and a POSIX port for Linux and macOS
|
||
(python3 only), including a theme-aware TUI and an Omarchy bar widget.
|
||
|
||
```
|
||
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)
|
||
claude-mode ollama # local server (preset: ollama)
|
||
claude-mode custom # your own endpoint (preset: custom)
|
||
```
|
||
|
||
**Contents:** [Install](#install) · [First run](#first-run) · [The interactive
|
||
menu](#the-interactive-menu) · [Commands](#commands) · [Presets](#presets-shipped) ·
|
||
[Context windows](#context-windows-and-early-auto-compaction) · [Z.AI](#zai-mode) ·
|
||
[LM Studio](#lm-studio-mode) · [Ollama](#ollama-mode) · [Custom
|
||
endpoints](#custom-endpoints) · [Adding a provider](#adding-a-provider) ·
|
||
[Live sessions](#live-sessions) ·
|
||
[Troubleshooting](#troubleshooting) · [Design decisions](#design-decisions) ·
|
||
[Layout](#layout) · [Linux / Omarchy](#linux--omarchy) · [The bar
|
||
widget](#the-omarchy-bar-widget) · [Uninstall](#uninstall)
|
||
|
||
---
|
||
|
||
## Install
|
||
|
||
### Windows (PowerShell 5.1+)
|
||
|
||
One line:
|
||
|
||
```powershell
|
||
irm https://git.nebulm.com/smoido/claude-mode/raw/branch/master/install.ps1 | iex
|
||
```
|
||
|
||
Piped in like that there is no checkout, so the installer fetches the
|
||
repository archive to `%TEMP%` and installs from it. To pass switches
|
||
(`-Force`, `-SkipKeyPrompt`), download first and run the file:
|
||
|
||
```powershell
|
||
irm https://git.nebulm.com/smoido/claude-mode/raw/branch/master/install.ps1 -OutFile install.ps1
|
||
.\install.ps1 -Force
|
||
```
|
||
|
||
Or clone it — the normal path if you want to read the code and pull updates:
|
||
|
||
```powershell
|
||
git clone https://git.nebulm.com/smoido/claude-mode.git
|
||
cd claude-mode
|
||
.\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`.
|
||
|
||
If the profile doesn't load: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`.
|
||
|
||
### Linux / macOS
|
||
|
||
One line:
|
||
|
||
```bash
|
||
curl -fsSL https://git.nebulm.com/smoido/claude-mode/raw/branch/master/linux/install.sh | bash
|
||
```
|
||
|
||
Or clone:
|
||
|
||
```bash
|
||
git clone https://git.nebulm.com/smoido/claude-mode.git
|
||
cd claude-mode
|
||
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.
|
||
|
||
It runs on the **bash 3.2 that macOS ships** — no Homebrew bash needed. The
|
||
menu reads arrow keys through the terminal's own timing (`min 0 time 1`) rather
|
||
than a fractional `read` timeout, which 3.2 rejects, and the port avoids
|
||
`mapfile` and GNU-only `stat`/`find` flags for the same reason.
|
||
|
||
Session listing reads `/proc`, so it is **Linux-only**. On macOS a switch says
|
||
it cannot check for running sessions instead of claiming there are none — so
|
||
restart Claude Code yourself after switching.
|
||
|
||
### Omarchy bar widget
|
||
|
||
```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). Needs a
|
||
checkout — clone first. See [the bar widget](#the-omarchy-bar-widget) for what it
|
||
does.
|
||
|
||
### All three, and upgrades
|
||
|
||
The installers never touch `~/.claude/settings.json` — that only happens on an
|
||
actual mode switch, which backs it up to `~/.claude-mode/backups/` first
|
||
(last 20 kept).
|
||
|
||
Re-running any of them is a safe upgrade: scripts are overwritten, existing
|
||
presets are kept unless `-Force` / `--force` is passed.
|
||
|
||
## First run
|
||
|
||
A shipped preset is a starting point, not a working configuration. OpenRouter
|
||
and Z.AI have no key stored. LM Studio's model ids are whatever happened to be
|
||
installed on the machine this was packaged on, which is almost certainly not
|
||
yours.
|
||
|
||
So the shipped presets carry `configured: false`, and preflight treats that as a
|
||
blocker with its own remedy:
|
||
|
||
```bash
|
||
claude-mode setup openrouter # key, then models from OpenRouter's catalogue
|
||
claude-mode setup zai # key, then per-tier GLM models
|
||
claude-mode setup lmstudio # server URL, auth, then models from that server
|
||
claude-mode setup anthropic # nothing to do; it uses your existing login
|
||
```
|
||
|
||
Setup asks only what it cannot work out, shows the current model map before
|
||
offering to change it, and picks from the provider's own catalogue rather than
|
||
asking anyone to type a model id from memory. LM Studio maps one model across
|
||
every tier, since a local server has one loaded at a time and per-tier mapping
|
||
would just pay the load cost on every tier change; the remote gateways ask per
|
||
tier, which is the point of them.
|
||
|
||
In a terminal, a switch that trips this offers to run setup there and then
|
||
rather than printing a command to type next. It sets `configured: true` on the
|
||
way out.
|
||
|
||
**Absent means configured.** Presets that predate this, and any built by hand
|
||
with `preset new`, are nobody's business but yours and will not start demanding
|
||
a wizard.
|
||
|
||
## 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.
|
||
|
||
## 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 (alias: lm-studio)
|
||
claude-mode ollama [preset] default preset: ollama
|
||
claude-mode custom [preset] default preset: custom
|
||
|
||
claude-mode presets list presets (* = active)
|
||
claude-mode preset show <name>
|
||
claude-mode preset new <name> [from] copy an existing preset (default: `default`)
|
||
claude-mode preset new <name> --provider <p> [--blank]
|
||
copy that provider's default, or start empty
|
||
claude-mode preset rename <name> <new-name> repoints state.json if it is in use
|
||
claude-mode preset default [provider] [name|--clear] which preset `claude-mode <provider>` picks
|
||
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] [key] store a key (hidden prompt; [key] for scripts)
|
||
claude-mode models [filter] models available from the active provider
|
||
claude-mode models --preset <name> ...from that preset's provider (and server) instead
|
||
claude-mode models --preset <name> --refresh only update the cached list the panel uses
|
||
claude-mode models --preset <name> --json the cached list for that provider, as JSON
|
||
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 per-provider default, never "most recently used".
|
||
Out of the box that is a fixed name — `claude-mode openrouter` means `default`,
|
||
`zai` means `zai`, `lmstudio` means `lmstudio` — and it can be changed:
|
||
|
||
```bash
|
||
claude-mode preset default # what each provider picks, and why
|
||
claude-mode preset default openrouter cheap # `claude-mode openrouter` now means `cheap`
|
||
claude-mode preset default openrouter --clear # back to the built-in name
|
||
```
|
||
|
||
The choice lives in `~/.claude-mode/defaults.json`. If the chosen preset is
|
||
deleted the choice is cleared with it, renaming moves it along, and a choice
|
||
whose file has gone some other way simply falls back to the built-in name. If
|
||
that is gone too, the provider's first preset by name is used. The Windows build
|
||
does not read this file yet, and keeps the built-in names.
|
||
|
||
Editing the active preset re-applies it straight away. For `preset set` and
|
||
`preset all` that happens **without** asking about running sessions: a switch
|
||
endangers them by moving their endpoint or key, and a tier edit moves neither.
|
||
`preset url` and `preset auth` do move them, so those still ask. If the re-apply
|
||
fails (a preflight refusal, say), the edit is still saved, and the last line says
|
||
so and names the command that finishes the job.
|
||
|
||
Every catalogue fetch — `models`, `doctor`, `setup`, the menu's picker — leaves
|
||
a copy in `~/.claude-mode/models-cache.json`, one entry per provider with its own
|
||
timestamp. A failed fetch keeps the previous list and marks it failed, and an LM
|
||
Studio list is tied to the server it came from. Model ids, context lengths and
|
||
prices only; no key or key name is ever written there.
|
||
|
||
## Presets shipped
|
||
|
||
One per mode, so `claude-mode <mode>` is never ambiguous and there is no
|
||
menu to read before the thing you asked for happens. Build more with
|
||
`preset new` whenever one stops being enough.
|
||
|
||
| preset | provider | opus | sonnet | haiku | fable |
|
||
|---|---|---|---|---|---|
|
||
| `default` | openrouter | `deepseek/deepseek-v4.1-flash` | `z-ai/glm-5.3-flash` | `openrouter/free` | `z-ai/glm-5.3` |
|
||
| `zai` | zai | `glm-5.3` | `glm-5.3` | `glm-4.7` | `glm-5.3` |
|
||
| `lmstudio` | lmstudio | whatever setup finds on your server (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`, `zai` | 1,000,000 |
|
||
| `lmstudio` | 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:
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
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.** The Qwen3.6
|
||
line above is what the single `lmstudio` preset ships pointed at, 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 the model you land on
|
||
does spam, run setup again and pick another:
|
||
|
||
```bash
|
||
claude-mode setup lmstudio
|
||
```
|
||
|
||
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:
|
||
|
||
```bash
|
||
claude-mode set-key lmstudio
|
||
# then in ~/.claude-mode/presets/lmstudio.json:
|
||
# "auth": { "mode": "vault", "keyRef": "lmstudio" }
|
||
```
|
||
|
||
### 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.
|
||
|
||
## Ollama mode
|
||
|
||
Ollama serves Anthropic's Messages API itself, at `/v1/messages`, so nothing sits
|
||
in between. `claude-mode setup ollama` asks where the server is (it ships on
|
||
`http://127.0.0.1:11434`), whether it needs a key, and picks one model for every
|
||
tier from what the server has pulled. One model for all four is the right shape
|
||
for a local server: it holds one in memory at a time, and mapping tiers to
|
||
different models just means paying the load cost on every tier change. The token
|
||
is a placeholder, `ollama`, which Ollama requires but does not check.
|
||
|
||
### The context window is set on the server
|
||
|
||
This is the part that bites. Ollama sizes the context on the server, not per
|
||
request: **4096 tokens** unless `ollama serve` runs with `OLLAMA_CONTEXT_LENGTH`,
|
||
and anything past it is cut off without an error. Claude Code's system prompt
|
||
alone is most of that. Ollama recommends 64k or more for Claude Code, and the
|
||
shipped preset declares 65,536 — but `contextTokens` only tells Claude Code what
|
||
to expect. It cannot change what the server does.
|
||
|
||
```bash
|
||
OLLAMA_CONTEXT_LENGTH=65536 ollama serve
|
||
# or, for the systemd service: systemctl edit ollama
|
||
# [Service]
|
||
# Environment=OLLAMA_CONTEXT_LENGTH=65536
|
||
```
|
||
|
||
`claude-mode doctor` reads what each loaded model is actually running with
|
||
(`/api/ps`) and the model's own maximum (`/api/show`), and says so when either is
|
||
below the preset. When nothing is loaded it cannot see the server's setting, and
|
||
says that instead of guessing.
|
||
|
||
Also worth knowing:
|
||
|
||
- A bare name like `qwen3-coder` is `qwen3-coder:latest` to Ollama; both work.
|
||
- Ollama does not implement `count_tokens` or prompt caching, so long sessions
|
||
redo more work than they would on a hosted provider. Nothing breaks.
|
||
- To reach it from another machine, serve it with `OLLAMA_HOST=0.0.0.0` and
|
||
point the preset there: `claude-mode preset url ollama http://192.168.1.40:11434`.
|
||
|
||
## Custom endpoints
|
||
|
||
For anything else that speaks Anthropic's Messages API: a LiteLLM or Vercel
|
||
gateway, vLLM, llama.cpp's server, a company proxy. The `custom` preset ships
|
||
with no address — there is no sensible one to guess — and a switch to it is
|
||
refused until it has one.
|
||
|
||
```bash
|
||
claude-mode setup custom # asks for all of the below
|
||
# or by hand:
|
||
claude-mode preset url custom https://llm.example.com
|
||
claude-mode preset auth custom key custom # the key lives in the vault as 'custom'
|
||
claude-mode set-key custom
|
||
claude-mode preset set custom opus <model-id> # ...and each other tier
|
||
```
|
||
|
||
Several endpoints are several presets, each with its own address and key name:
|
||
`claude-mode preset new work --provider custom --blank`, then
|
||
`claude-mode preset auth work key work`.
|
||
|
||
The model list comes from `/v1/models` when the endpoint has one (OpenAI's shape
|
||
or Anthropic's). Plenty of proxies serve Messages and nothing else. That is fine:
|
||
the check before a switch only wants something to answer, and model ids can be
|
||
typed by hand. The cost guard still applies. A custom endpoint in front of
|
||
Anthropic's own models needs `"allowAnthropicModels": true` in the preset, which
|
||
keeps it deliberate.
|
||
|
||
## Adding a provider
|
||
|
||
Every gateway provider is an entry in [`providers.json`](providers.json), installed
|
||
next to the presets and read by the CLI, the bar widget (through `health.json`)
|
||
and the Windows build. Anthropic is not in it: it is the native login, not a
|
||
gateway. An entry holds:
|
||
|
||
| field | what it says |
|
||
|---|---|
|
||
| `id`, `aliases`, `title`, `label`, `blurb`, `color` | names and how the CLI, menu and panel show it |
|
||
| `defaultPreset`, `preset` | the preset `claude-mode <id>` picks, and the template for a blank one (URL, auth, context, extra env) |
|
||
| `server` | whether the address is editable, how it is probed before a switch (`always`, `lenient` for proxies, `local` only when it is on this machine), setup hint |
|
||
| `catalogue.kind` | how its model list is read: `openrouter`, `lmstudio`, `ollama`, `openai` (`/v1/models`) or `static` (a list kept in the entry) |
|
||
| `setup` | whether a key is required or optional, where to get one, and one model for every tier or one per tier |
|
||
| `doctor` | which checks apply: `catalogue-models`, `openrouter-key`, `guardrail`, `message-check`, `ollama-context`, `lmstudio-templates` (Windows) |
|
||
| `logo`, `logoScale` | a single-path 24×24 SVG mark, and its optical size correction |
|
||
|
||
A provider that reuses those kinds is an entry plus a default preset in
|
||
`presets/`, and no code. A hosted Anthropic-compatible coding plan — Kimi,
|
||
MiniMax, DeepSeek — is an entry shaped like `zai`, with its own URL and a
|
||
`static` or `openai` catalogue. Only a genuinely new kind of behaviour, such as
|
||
a catalogue format none of the parsers read, needs code: a parser in
|
||
`cm-json.py`, and a branch in `provider_catalogue` (bash) and
|
||
`Get-ProviderCatalogue` (Windows). The bar widget picks new providers up from
|
||
`health.json` — no QML change, and no shell restart.
|
||
|
||
## Live sessions
|
||
|
||
### Why a switch breaks a running session
|
||
|
||
A switch **breaks** running sessions. The two halves of the config behave
|
||
differently, which is why.
|
||
|
||
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 affected sessions — and
|
||
`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.
|
||
|
||
### The part that is not just an inconvenience
|
||
|
||
A failed call is recoverable. A *successful* one may not be.
|
||
|
||
If a running session takes even one completion from the new provider before
|
||
anything notices — which happens when the mode it is switched to matches the
|
||
base URL it already had cached — that provider's message-id format lands in its
|
||
transcript. OpenRouter issues `gen-<epoch>-<rand>` where Anthropic issues
|
||
`msg_…`. Native Anthropic then refuses to resume the session at all:
|
||
|
||
```
|
||
API Error: 400 diagnostics.previous_message_id: must be the `id` from a
|
||
prior /v1/messages response (starts with `msg_`)
|
||
```
|
||
|
||
There is no supported way back from that. The transcript has to be rolled back
|
||
to the last message Anthropic issued:
|
||
|
||
```bash
|
||
claude-mode repair-session # transcripts for this project
|
||
claude-mode repair-session --all # every project, problems only
|
||
claude-mode repair-session <session-id> # show what it would cut
|
||
claude-mode repair-session <session-id> --apply
|
||
```
|
||
|
||
Scope, since it is not obvious: a bare listing covers only the project you are
|
||
standing in (walking up from the current directory to find it), while a **named
|
||
session id is looked up across every project** — you rarely remember which
|
||
project a session you cannot resume belonged to. `--all` drops the scoping
|
||
entirely.
|
||
|
||
Not every broken session is worth repairing — a throwaway, or one whose work
|
||
was finished some other way — and one that can never be cleared keeps the bar's
|
||
warning dot lit for good. So a session can be dismissed, and one untouched for
|
||
more than 7 days is hidden on its own:
|
||
|
||
```bash
|
||
claude-mode repair-session --ignore <session-id> # stop counting it
|
||
claude-mode repair-session --ignored # what is dismissed, and whether it is still broken
|
||
claude-mode repair-session --unignore <session-id> # or --unignore-all
|
||
claude-mode repair-session --all --max-age 0 # include the age-hidden ones (CM_IGNORE_AGE_DAYS sets the default)
|
||
```
|
||
|
||
Neither touches the transcript. Hidden sessions are always named — `--all`
|
||
ends with a line like `2 hidden: 1 ignored, 1 older than 7 days` — so age-hiding
|
||
never looks like damage disappearing. Dismissals live in
|
||
`~/.claude-mode/ignored-sessions.json`; an entry is dropped when its transcript
|
||
is deleted, and when the session is repaired, so a session that breaks again
|
||
later is not silently hidden. In the bar panel each broken session has an
|
||
**Ignore** button beside **Repair**, and the hidden ones sit under a collapsed
|
||
`hidden (N)` row with **Restore**.
|
||
|
||
`--all` reports only what is actually actionable, which matters more than it
|
||
sounds. Of 59 transcripts here it initially flagged 16; on inspection 5 had
|
||
simply never received a reply, and 10 had run start-to-finish on a gateway, so
|
||
every id in them is that provider's by design. Those resume perfectly well under
|
||
the provider they were born on, have nothing to truncate back to, and are only a
|
||
problem if you try to resume them as Anthropic. Neither is damage, so neither is
|
||
listed. Only a transcript that has a genuine `msg_` message *and* junk after it
|
||
is something this can or should touch.
|
||
|
||
### The cut turns are not thrown away
|
||
|
||
Truncating is the mechanical fix, but the turns being cut are the work itself —
|
||
losing the conversation that produced a morning's changes is most of the damage,
|
||
and a session that resumes with a hole in its memory is barely resumed at all.
|
||
So `--apply` does three things before it deletes anything:
|
||
|
||
1. **Backs up** the original as `<session>.jsonl.pre-repair-backup-<stamp>`.
|
||
2. **Writes the dropped turns out** as `<session>.recovered-<stamp>.md` — a
|
||
readable record of what was asked, what was answered, and what was run. Tool
|
||
*results* are left out; they are most of a transcript by volume and the least
|
||
useful part of a summary.
|
||
3. **Hands them back to the session** as a single appended note, so the agent
|
||
that resumes knows what it just did.
|
||
|
||
That note is a `user` entry marked `isMeta` — the same marker Claude Code uses
|
||
for its own local-command caveats, meaning "context, not something to answer".
|
||
Critically it carries **no `message.id`**, so it cannot re-create the very
|
||
condition being repaired. `--no-reinject` writes the Markdown but leaves the
|
||
session untouched.
|
||
|
||
It refuses to touch a transcript written to in the last 90 seconds, since that
|
||
one belongs to a session still alive.
|
||
|
||
### The switch asks first
|
||
|
||
A switch stops and asks while any sessions are running, offering to restart them
|
||
(the only answer that ends with everything on the mode the bar now claims), close
|
||
them, proceed anyway, or abort — and abort is the default. Non-interactively it
|
||
refuses outright unless given `--yes`.
|
||
|
||
```
|
||
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 bar notices for you
|
||
|
||
Nothing tells you a session is unresumable until you try to resume it, by which
|
||
point you have usually forgotten which one it was. So the widget scans every
|
||
project on a timer (and whenever the panel opens) and puts a dot on its icon
|
||
when there is something to fix. Clicking through lists the affected sessions and
|
||
offers to repair each one, after saying what it will drop and what it will keep.
|
||
|
||
The dot is a dot rather than a colour change, because recolouring the mark would
|
||
misreport the active mode — which is the widget's actual job.
|
||
|
||
That scan is only affordable because it reads the *tail* of each transcript
|
||
first: if the last message is Anthropic's, the transcript is healthy and the
|
||
rest of the file is never opened. Since that is the overwhelmingly common case,
|
||
the whole sweep costs ~60ms for 59 transcripts, against ~5s for the obvious
|
||
version that reads every byte of every one.
|
||
|
||
This is why a switch asks before it writes rather than reporting afterwards.
|
||
|
||
## Troubleshooting
|
||
|
||
### When the helper "is failing"
|
||
|
||
Claude Code reports a broken helper as *your apiKeyHelper script is failing* and
|
||
shows its stderr under `/status`. Two causes account for nearly all of it.
|
||
|
||
**A space in your home directory.** `apiKeyHelper` is a shell *command line*, not
|
||
a path, so `C:\Users\Firstname Lastname\.claude-mode\bin\claude-key-helper.cmd`
|
||
is split at the space and cmd tries to run `C:\Users\Firstname`. The value is now
|
||
quoted when it needs to be, on both ports — `shlex.quote` on POSIX, where a
|
||
`/Users/Firstname Lastname` home does the same thing. Paths that need no quoting
|
||
are written bare exactly as before, so no existing `settings.json` churns.
|
||
|
||
`doctor` used to miss this, because it quoted the path itself before running it
|
||
and so tested something Claude Code never sees. It now reads the string out of
|
||
`settings.json`, says so when that string is not what a switch would write, and
|
||
executes *that* string through a shell. Re-running the switch rewrites it:
|
||
|
||
```
|
||
claude-mode openrouter default
|
||
claude-mode doctor
|
||
```
|
||
|
||
**A key stored by a different Windows account.** The vault is DPAPI `CurrentUser`
|
||
scope, so a key stored from an elevated or *run as* shell cannot be decrypted by
|
||
the account Claude Code runs as. Re-run `claude-mode set-key <ref>` unelevated,
|
||
as yourself.
|
||
|
||
Other known issues with their own sections: [context windows and early
|
||
compaction](#context-windows-and-early-auto-compaction), the [LM Studio template
|
||
parser spam](#the-server-error--unable-to-generate-parser-for-this-template-spam),
|
||
and [refusing a switch that would not work](#refusing-a-switch-that-would-not-work).
|
||
|
||
## Design decisions
|
||
|
||
### 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.
|
||
|
||
### 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 |
|
||
|---|---|---|
|
||
| any gateway | the preset has been through setup at least once | `claude-mode setup <mode>` |
|
||
| `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.
|
||
|
||
### 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.)
|
||
|
||
## Layout
|
||
|
||
Windows:
|
||
|
||
```
|
||
~/.claude-mode/
|
||
claude-mode.ps1 main script
|
||
providers.json what each gateway provider is (replaced on every install)
|
||
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
|
||
```
|
||
|
||
Linux / macOS: the same tree, with `bin/claude-mode` + `bin/claude-key-helper.sh`,
|
||
`bin/cm-json.py`, `bin/cm-vault.sh`, the symlink `~/.local/bin/claude-mode`, and
|
||
the wrapper block in `~/.bashrc` / `~/.zshrc`.
|
||
|
||
`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. Install is [up top](#linux--macos).
|
||
|
||
### 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.
|
||
|
||
## 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. Install is
|
||
[up top](#omarchy-bar-widget). The shell notices the new files and reloads the
|
||
plugin, but on an **upgrade** that is not enough: it clears Qt's component cache
|
||
while the old widget is still alive, so the old compiled panel survives, and
|
||
`omarchy-shell shell rescanPlugins` does not shift it either. Run
|
||
`omarchy restart shell` after upgrading. The same goes for any change to
|
||
`Modes.js`, which the QML engine caches for the life of the process as a
|
||
`.pragma library`.
|
||
|
||
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 not been set up, has no key stored, or its
|
||
server is not answering, the panel says which and offers the fix: *Set up
|
||
<mode>…* opens a terminal and runs the whole first-run flow there (a bar popup
|
||
can host neither a hidden key prompt nor a filter-select model list), *Store
|
||
the key…* opens a terminal for just the prompt, *Check again* re-runs the
|
||
preflight, and *Server settings…* opens the form below. For an LM Studio
|
||
preset the same form is also one click inside the preset editor (below),
|
||
without waiting for a failure.
|
||
|
||
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.
|
||
|
||
- **The gear** on any preset row opens that preset in an editor: its four tiers,
|
||
each with the model it maps to. Clicking a tier opens one field that filters
|
||
the provider's models as you type (every word must match; ↓/↑ and Enter work),
|
||
listed with context length and price. That list comes from
|
||
the cache the CLI leaves behind, so the panel never touches the network on
|
||
its own. *Fetch models* / *Refresh list* runs `models --refresh` when it is
|
||
empty or old. Any id can also be typed by hand. The gear sits on each row
|
||
rather than each provider, because two presets of one provider map tiers
|
||
differently, and two LM Studio presets can point at two different machines.
|
||
Editing the preset in use applies it at once; see
|
||
[Commands](#commands) for why that does not ask about running sessions.
|
||
- **Esc** closes the panel. While it is open the panel holds the keyboard, as
|
||
every other shell panel does, so its text fields can be typed into.
|
||
- **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
|
||
|
||
Windows:
|
||
|
||
```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
|
||
```
|
||
|
||
Linux / macOS:
|
||
|
||
```bash
|
||
claude-mode anthropic # clean settings.json first
|
||
rm -rf ~/.claude-mode ~/.local/bin/claude-mode
|
||
# then delete the block between the >>> claude-mode >>> markers in ~/.bashrc / ~/.zshrc
|
||
```
|
||
|
||
Bar widget:
|
||
|
||
```bash
|
||
rm -rf ~/.config/omarchy/plugins/smoido.claude-mode
|
||
# then remove the {"id": "smoido.claude-mode"} entry from ~/.config/omarchy/shell.json
|
||
```
|