Each provider's preset list ends in "New ... preset...", and the preset editor gains Duplicate, Rename and Delete. New asks for a name and what to start from: a copy of one of that provider's presets, or a blank template. Delete is greyed out, with the reason, on the preset in use, and warns when it would leave a provider with no preset at all. CLI: - preset rename <name> <new>: the new name is hard-linked in, state.json is repointed, and only then does the old name go. - preset new <name> --provider <p> [--blank]: with a provider and no source it copies that provider's own default instead of the OpenRouter-only `default`; --blank starts from the scaffold. - valid_preset_name on every preset subcommand: no slash, no leading dot. `preset show ../../etc/passwd` used to print the file. - preset rm warns when it removes a provider's last preset. - preflight refuses a preset with every tier empty. Otherwise a blank preset would switch cleanly and Claude Code would ask the gateway for its default Anthropic models, billed at full price on OpenRouter. The panel's blocked card offers "Edit preset..." for it. The key helper now reads the active preset in a single open (new cm-json auth-of) and re-reads state.json once on a miss. Renaming the active preset could otherwise catch a running session between reading the old name and opening the file: 1 failure in 51 key fetches in a race test before, 0 in 118 across 60 renames after. It could also briefly hand out the openrouter key for a preset that uses another. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
897 lines
41 KiB
Markdown
897 lines
41 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.
|
|
|
|
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)
|
|
```
|
|
|
|
**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) · [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
|
|
|
|
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 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 **fixed** per-provider default, not "most recently
|
|
used" — so `claude-mode openrouter` always means `default`.
|
|
|
|
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.
|
|
|
|
## 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
|
|
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
|
|
```
|