README: install commands first, sections reorganized

This commit is contained in:
smoido
2026-09-07 20:37:11 +03:00
parent b60c00450c
commit 69c7810345
2 changed files with 361 additions and 305 deletions
+346 -304
View File
@@ -3,9 +3,10 @@
Switch Claude Code system-wide between **Anthropic**, **OpenRouter**, **Z.AI**, and a Switch Claude Code system-wide between **Anthropic**, **OpenRouter**, **Z.AI**, and a
local **LM Studio** server — with named per-tier model presets. local **LM Studio** server — with named per-tier model presets.
Windows / PowerShell 5.1 with no external dependencies, and a POSIX port for One write to `~/.claude/settings.json` covers the CLI, the VS Code extension, and
Linux and macOS that needs only python3 — including a theme-aware TUI and an the desktop app. Restart Claude Code afterwards — nothing else. Windows
Omarchy bar widget. See [Linux / Omarchy](#linux--omarchy). (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 # interactive menu
@@ -15,11 +16,124 @@ claude-mode zai # Z.AI GLM coding plan (preset: zai)
claude-mode lmstudio # local server (preset: lmstudio) claude-mode lmstudio # local server (preset: lmstudio)
``` ```
Works for the CLI, the VS Code extension, and the desktop app from a single **Contents:** [Install](#install) · [First run](#first-run) · [The interactive
switch. Restart Claude Code afterwards — nothing else. 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.
### 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 ## The interactive menu
Run `claude-mode` with no arguments. The mode you're already in is omitted — Run `claude-mode` with no arguments. The mode you're already in is omitted —
@@ -45,84 +159,6 @@ edit the preset that's currently live it re-applies immediately.
When stdin is redirected (scripts, CI) the menu is skipped and `status` prints When stdin is redirected (scripts, CI) the menu is skipped and `status` prints
instead, so `claude-mode` is still safe in a pipeline. instead, so `claude-mode` is still safe in a pipeline.
## Why settings.json and not a profile export
Three ways to make this persist. I picked the second.
**1. Export the variables from `$PROFILE`.**
The obvious move, and the wrong one here. It only covers processes launched from
a PowerShell session that loaded the profile — which is exactly *not* how you use
Claude Code. The VS Code extension is spawned by VS Code, not by your shell, so
it would never see the exports. Same for the desktop app, `cmd.exe`, and any
terminal opened before the switch. Worse, the failure is silent: you switch to
`anthropic`, a shell opened five minutes ago still has `ANTHROPIC_BASE_URL` set,
and that session quietly keeps billing OpenRouter.
**2. Rewrite the `env` block in `~/.claude/settings.json`.** ← chosen
Claude Code reads this file on every startup, from every launch context. One
write, and the next `claude` — CLI, extension, desktop — picks it up. A switch is
atomic: one file, one source of truth. `claude-mode anthropic` *deletes* the
managed keys rather than blanking them, so nothing can linger and break native
auth. The cost is that config is global rather than per-terminal.
**3. Persistent User-scope environment variables (`setx`).**
Also global and reboot-proof, but strictly worse: new processes only, values sit
in the registry in plaintext, and a stale entry silently outranks whatever
`claude-mode` writes. This tool treats them as a fault condition — `status` and
`doctor` flag them and offer removal, backing the old value up first.
The `claude` wrapper in the profile is a **safety net, not the mechanism**. It
strips inherited process-level copies of all thirteen managed variables before
launching `claude.exe`. Everything still works without it — including in VS Code,
which never loads the profile.
## Why API keys are not in settings.json
`settings.json` is a config file you'll hand-edit, diff, and possibly paste into
a bug report. A `sk-or-` or Z.AI token does not belong there.
Keys are stored **DPAPI-encrypted** in `~/.claude-mode/vault/*.cred` — encrypted
against your Windows account on this machine, so copying the file elsewhere or
reading it as another user yields nothing — with the file ACL restricted to you.
Claude Code receives the key at runtime through `apiKeyHelper`, which decrypts
and prints it. `settings.json` holds only the base URL and model IDs.
In `anthropic` mode the helper is removed from settings.json *and* returns
nothing when state says `anthropic` — belt and braces. LM Studio's `lmstudio`
token is a placeholder, not a secret, so it's written inline and the helper stays
out of it.
### 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.
Every helper failure path now names itself on stderr rather than exiting 1 in
silence, so `/status` distinguishes these from a missing preset or an empty
vault. The success paths stay silent — stdout carries the key and nothing else.
## Commands ## Commands
``` ```
@@ -207,7 +243,7 @@ background tasks. Switching without `contextTokens` prints a warning.
Adjust per preset: Adjust per preset:
```powershell ```bash
# edit ~/.claude-mode/presets/<name>.json -> "contextTokens": 262144 # edit ~/.claude-mode/presets/<name>.json -> "contextTokens": 262144
claude-mode doctor # re-checks declared vs actual claude-mode doctor # re-checks declared vs actual
``` ```
@@ -229,7 +265,7 @@ Per [Z.AI's Claude Code docs](https://docs.z.ai/devpack/tool/claude):
All three extra variables are confirmed present in CLI 2.1.221. Setup: All three extra variables are confirmed present in CLI 2.1.221. Setup:
```powershell ```bash
claude-mode set-key zai # paste your key from https://z.ai/manage-apikey/apikey-list claude-mode set-key zai # paste your key from https://z.ai/manage-apikey/apikey-list
claude-mode zai claude-mode zai
claude-mode doctor # sends a 1-token request to prove the key works claude-mode doctor # sends a 1-token request to prove the key works
@@ -286,7 +322,7 @@ 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 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 reproduce the failure against it here — cold JIT, streaming, tools, system
blocks, and multi-turn `tool_result` all succeeded. Whether it trips seems to blocks, and multi-turn `tool_result` all succeeded. Whether it trips seems to
depend on which parser strategy the runtime picks. If the model setup lands on depend on which parser strategy the runtime picks. If the model you land on
does spam, run setup again and pick another: does spam, run setup again and pick another:
```bash ```bash
@@ -298,67 +334,37 @@ 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: If you enable authentication in LM Studio, move that preset to a vault key:
```powershell ```bash
claude-mode set-key lmstudio claude-mode set-key lmstudio
# then in ~/.claude-mode/presets/lmstudio.json: # then in ~/.claude-mode/presets/lmstudio.json:
# "auth": { "mode": "vault", "keyRef": "lmstudio" } # "auth": { "mode": "vault", "keyRef": "lmstudio" }
``` ```
## CLI version ### LM Studio somewhere other than this machine
Verified against `claude.exe` **2.1.221** by scanning the binary — every variable It ships on loopback, but that is a default, not a constraint. A preset is just
this tool writes is referenced by it: a base URL and an auth block, so all of these are the same two fields:
`ANTHROPIC_BASE_URL` · `ANTHROPIC_AUTH_TOKEN` · `ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL` · ```bash
`CLAUDE_CODE_SUBAGENT_MODEL` · `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` · claude-mode preset url lmstudio http://192.168.1.40:1234 # another box on the LAN
`CLAUDE_CODE_ATTRIBUTION_HEADER` · `CLAUDE_CODE_AUTO_COMPACT_WINDOW` · claude-mode preset url lmstudio https://lms.example.net # through a tunnel or proxy
`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` · `API_TIMEOUT_MS` · `apiKeyHelper` claude-mode preset auth lmstudio key lmstudio # that server wants an API key
claude-mode set-key lmstudio # store it (vault, not settings.json)
(On 2.1.89 the fable and gateway-discovery vars did not exist; the update to claude-mode preset auth lmstudio none # back to the open-server default
2.1.221 added both.)
## Setup
One line, on any machine that can reach the repo (PowerShell 5.1 is fine):
```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 `auth none` writes LM Studio's inline placeholder token, which is not a secret
repository archive to `%TEMP%` and installs from it. To pass switches and is what an unauthenticated server expects. `auth key` moves it to the vault
(`-Force`, `-SkipKeyPrompt`), download first and run the file: 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.
```powershell ## Live sessions
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: ### Why a switch breaks a running session
```powershell A switch **breaks** running sessions. The two halves of the config behave
git clone https://git.nebulm.com/smoido/claude-mode.git differently, which is why.
cd claude-mode
.\install.ps1
```
All three install the same way: to `~/.claude-mode/` (ACL: you only), dropping
`claude-mode.cmd` into `~/.local/bin` (already on your User PATH, next to
`claude.exe`), and adding a marked block to
`~/Documents/WindowsPowerShell/profile.ps1`.
`~/.claude/settings.json` is **not** touched by the installer — only by an actual
mode switch, which backs it up to `~/.claude-mode/backups/` first (last 20 kept).
Re-running any of them is a safe upgrade: scripts are overwritten, existing
presets are kept unless `-Force` is passed.
If the profile doesn't load: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`.
## Restarting sessions
A switch **breaks** running sessions. This originally said it did not affect
them; that was wrong, and the difference matters, because the two halves of the
config behave differently.
The static half — base URL, model ids, the env block — genuinely is read once at 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. startup, and a running session keeps what it started with.
@@ -375,7 +381,14 @@ switch reaches into a live session through the one thing that was never cached:
| another preset of the *same* provider, same `keyRef` | same key, same endpoint — this one survives, on the model ids it started with | | 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: Either of the first two starts failing calls whenever the TTL happens to expire:
mid-turn as easily as between turns. 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 ### The part that is not just an inconvenience
@@ -441,6 +454,44 @@ session untouched.
It refuses to touch a transcript written to in the last 90 seconds, since that It refuses to touch a transcript written to in the last 90 seconds, since that
one belongs to a session still alive. 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 ### The bar notices for you
Nothing tells you a session is unresumable until you try to resume it, by which Nothing tells you a session is unresumable until you try to resume it, by which
@@ -458,123 +509,92 @@ 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 the whole sweep costs ~60ms for 59 transcripts, against ~5s for the obvious
version that reads every byte of every one. version that reads every byte of every one.
This is why a switch now asks before it writes rather than reporting afterwards. This is why a switch asks before it writes rather than reporting afterwards.
So restart affected sessions — and `claude-mode sessions --restart` will do it
for you:
- **CLI** — exit and relaunch `claude` ## Troubleshooting
- **VS Code** — `Ctrl+Shift+P`*Developer: Reload Window*
- **Desktop app** — quit and reopen
`claude-mode status` shows what the *next* launch will use. ### When the helper "is failing"
## Layout 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/ claude-mode openrouter default
claude-mode.ps1 main script claude-mode doctor
state.json mode, active preset, and the exact env keys last written
presets/*.json provider + model maps
vault/*.cred DPAPI-encrypted keys (openrouter, zai, ...)
backups/ settings.json snapshots + removed env-var values
bin/claude-key-helper.cmd apiKeyHelper shim
~/.local/bin/claude-mode.cmd PATH entry point (works from any shell)
~/Documents/WindowsPowerShell/profile.ps1
claude-mode + claude functions, between markers
``` ```
`state.json` records which env keys the last switch actually wrote, so a custom **A key stored by a different Windows account.** The vault is DPAPI `CurrentUser`
`extraEnv` key (Z.AI's timeouts, LM Studio's attribution header) is removed when scope, so a key stored from an elevated or *run as* shell cannot be decrypted by
you switch away — even though no other preset knows that key exists. the account Claude Code runs as. Re-run `claude-mode set-key <ref>` unelevated,
as yourself.
## Linux / Omarchy 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).
The `linux/` tree is a POSIX port of the same design: one write to ## Design decisions
`~/.claude/settings.json`, secrets kept out of it, presets shared verbatim with
the Windows build.
```bash ### Why settings.json and not a profile export
cd claude-code-switcher
bash linux/install.sh # --skip-key-prompt to install without storing a key
```
Installs to `~/.claude-mode/`, symlinks `~/.local/bin/claude-mode`, and adds the Three ways to make this persist. I picked the second.
`claude` wrapper to `~/.bashrc` / `~/.zshrc` between markers.
Secrets have no DPAPI equivalent here, so the vault picks the best backend **1. Export the variables from `$PROFILE`.**
available and says which one it chose: macOS Keychain, libsecret, `pass`, or a The obvious move, and the wrong one here. It only covers processes launched from
0600 file that is honestly labelled as unencrypted. 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.
### The menu follows your desktop theme **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.
The sixteen ANSI colour slots carry no guarantee about relative brightness, and **3. Persistent User-scope environment variables (`setx`).**
monochrome themes exploit that. Under Omarchy's Solitude, slot 36 — headings — Also global and reboot-proof, but strictly worse: new processes only, values sit
resolves to `#707070` and slot 31 — `FAIL` — to `#565d60`. Against `#cacccc` in the registry in plaintext, and a stale entry silently outranks whatever
body text on a `#101315` ground that is 3.8:1 and 2.8:1 where the body text is `claude-mode` writes. This tool treats them as a fault condition — `status` and
11.6:1, so headings render as fine print and an error becomes the quietest `doctor` flag them and offer removal, backing the old value up first.
thing on screen. Exactly backwards.
So when Omarchy is present, the palette is derived from the theme it publishes The `claude` wrapper in the profile is a **safety net, not the mechanism**. It
at `~/.local/state/omarchy/current/theme/colors.toml` instead. Every role is strips inherited process-level copies of all thirteen managed variables before
measured against the background it will actually be drawn on and lifted toward launching `claude.exe`. Everything still works without it — including in VS Code,
the foreground when it falls short, which keeps hue where the theme has any and which never loads the profile.
falls back to weight where it does not:
| role | before (Solitude) | after | ### Why API keys are not in settings.json
|---|---|---|
| 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 `settings.json` is a config file you'll hand-edit, diff, and possibly paste into
authoritative, so `catppuccin-latte` and `flexoki-light` keep their accents a bug report. A `sk-or-` or Z.AI token does not belong there.
rather than washing out.
Overrides: `CLAUDE_MODE_THEME=/path/to/colors.toml` points it elsewhere, Keys are stored **DPAPI-encrypted** in `~/.claude-mode/vault/*.cred` — encrypted
`NO_COLOR` turns it off. Without Omarchy, or on a terminal that cannot do against your Windows account on this machine, so copying the file elsewhere or
truecolor, it falls back to the ANSI slots with the two roles the slots get reading it as another user yields nothing — with the file ACL restricted to you.
wrong corrected — bright red for `FAIL`, bold on headings. Claude Code receives the key at runtime through `apiKeyHelper`, which decrypts
and prints it. `settings.json` holds only the base URL and model IDs.
### `claude-mode health` 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.
Rewrites `~/.claude-mode/health.json`, the machine-readable mirror of the ### Refusing a switch that would not work
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.
## First-run setup
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.
## Refusing a switch that would not work
A switch writes settings.json and is picked up by the *next* `claude` launch, so 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 switching into a mode that cannot serve requests does not fail loudly — it
@@ -616,83 +636,94 @@ 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 nothing behind it, and produces the identical silent breakage. Off-machine
addresses get a longer timeout, not a pass. addresses get a longer timeout, not a pass.
### LM Studio somewhere other than this machine ### CLI version
It ships on loopback, but that is a default, not a constraint. A preset is just Verified against `claude.exe` **2.1.221** by scanning the binary — every variable
a base URL and an auth block, so all of these are the same two fields: this tool writes is referenced by it:
```bash `ANTHROPIC_BASE_URL` · `ANTHROPIC_AUTH_TOKEN` · `ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL` ·
claude-mode preset url lmstudio http://192.168.1.40:1234 # another box on the LAN `CLAUDE_CODE_SUBAGENT_MODEL` · `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` ·
claude-mode preset url lmstudio https://lms.example.net # through a tunnel or proxy `CLAUDE_CODE_ATTRIBUTION_HEADER` · `CLAUDE_CODE_AUTO_COMPACT_WINDOW` ·
claude-mode preset auth lmstudio key lmstudio # that server wants an API key `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` · `API_TIMEOUT_MS` · `apiKeyHelper`
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 (On 2.1.89 the fable and gateway-discovery vars did not exist; the update to
and is what an unauthenticated server expects. `auth key` moves it to the vault 2.1.221 added both.)
like every other credential — a real key on a public address is a real key.
Nothing here changes the shipped presets unless you ask it to; the local default
stays exactly as it was.
## Sessions still on the old provider ## Layout
A switch does not leave running sessions on the old provider — it breaks them. Windows:
See [Restarting sessions](#restarting-sessions) for the mechanism: their
endpoint is fixed at startup but their credential is re-fetched on a timer, so
it switches under them and the endpoint they are still pointed at refuses it.
Which is why this is a decision rather than a notification.
A switch stops and asks while any 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 ~/.claude-mode/
running sessions (2) claude-mode.ps1 main script
562250 pts/4 /home/smoido/Work working (this session - never touched) state.json mode, active preset, and the exact env keys last written
631644 pts/1 /home/smoido/Projects/api 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
``` ```
Sessions are found through `/proc/<pid>/exe`, which on Linux resolves to the Linux / macOS: the same tree, with `bin/claude-mode` + `bin/claude-key-helper.sh`,
real `claude` binary — a process-name match would sweep up every shell that `bin/cm-json.py`, `bin/cm-vault.sh`, the symlink `~/.local/bin/claude-mode`, and
merely mentions claude on its command line, including the one this is running the wrapper block in `~/.bashrc` / `~/.zshrc`.
from. Two things are then filtered out:
- **The calling session.** Killing the session that asked for the kill is not a `state.json` records which env keys the last switch actually wrote, so a custom
thing anyone means, so it is listed and never signalled. `extraEnv` key (Z.AI's timeouts, LM Studio's attribution header) is removed when
- **Forks of a session.** A busy session spawns children off its own binary, you switch away — even though no other preset knows that key exists.
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 ## Linux / Omarchy
— 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 `linux/` tree is a POSIX port of the same design: one write to
the way down). `--restart` stops each session and reopens it — re-running the `~/.claude/settings.json`, secrets kept out of it, presets shared verbatim with
parent terminal's own command line where there is one, so the same terminal, the Windows build. Install is [up top](#linux--macos).
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 ### The menu follows your desktop theme
callers that have already asked, and `--dry-run` prints the plan and touches
nothing. 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 ## The Omarchy bar widget
An icon in the Omarchy top bar showing which provider the next `claude` launch 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. will use, and a panel that switches it without a terminal. Install is
[up top](#omarchy-bar-widget). Both install steps hot-reload, so nothing needs
```bash restarting — except after a change to `Modes.js`, which the QML engine caches
bash omarchy/install.sh for the life of the process as a `.pragma library` (`omarchy restart shell`).
```
Copies the plugin to `~/.config/omarchy/plugins/smoido.claude-mode/` and adds
its id to the bar layout in `~/.config/omarchy/shell.json` (backed up first).
Both hot-reload, so nothing needs restarting — except after a change to
`Modes.js`, which the QML engine caches for the life of the process as a
`.pragma library` (`omarchy restart shell`).
The icon is the mode, and it is the provider's own logo: the Claude burst, the 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 OpenRouter arrow, the Z.AI Z, the LM Studio mark. They are drawn as vector paths
@@ -737,9 +768,9 @@ CLI does, and both can stop it:
<mode>…* opens a terminal and runs the whole first-run flow there (a bar popup <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 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 the key…* opens a terminal for just the prompt, *Check again* re-runs the
preflight, and *Server settings…* opens the form below. The gear on any LM Studio preset row opens the same form without preflight, and *Server settings…* opens the form below. The gear on any LM
waiting for a failure — per row, because two LM Studio presets can point at Studio preset row opens the same form without waiting for a failure — per
two different machines. row, because two LM Studio presets can point at two different machines.
The form holds the base URL, a *Use local default* reset, and a switch for 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 whether that server needs an API key. Saving rewrites the preset and drops
@@ -749,6 +780,7 @@ CLI does, and both can stop it:
*Switch and close*, *Switch only*, or *Cancel*. The session action is applied *Switch and close*, *Switch only*, or *Cancel*. The session action is applied
strictly **after** the write — restarting first would only bring them back up strictly **after** the write — restarting first would only bring them back up
on the provider you just left. on the provider you just left.
- **Right-click** switches straight back to Anthropic. - **Right-click** switches straight back to Anthropic.
- **Middle-click** re-reads state. - **Middle-click** re-reads state.
- **Hover** for mode, preset, and the opus/sonnet mapping. - **Hover** for mode, preset, and the opus/sonnet mapping.
@@ -771,18 +803,28 @@ Placement is `right`, before `omarchy.agents`; override with `CM_BAR_SECTION`
and `CM_BAR_BEFORE` when installing. Moving it later is a normal and `CM_BAR_BEFORE` when installing. Moving it later is a normal
`omarchy bar move smoido.claude-mode --section <section>`. `omarchy bar move smoido.claude-mode --section <section>`.
Uninstall:
```bash
rm -rf ~/.config/omarchy/plugins/smoido.claude-mode
# then remove the {"id": "smoido.claude-mode"} entry from ~/.config/omarchy/shell.json
```
## Uninstall ## Uninstall
Windows:
```powershell ```powershell
claude-mode anthropic # clean settings.json first claude-mode anthropic # clean settings.json first
Remove-Item ~\.claude-mode -Recurse -Force Remove-Item ~\.claude-mode -Recurse -Force
Remove-Item ~\.local\bin\claude-mode.cmd Remove-Item ~\.local\bin\claude-mode.cmd
# then delete the block between the >>> claude-mode >>> markers in profile.ps1 # 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
```
+15 -1
View File
@@ -20,7 +20,6 @@ for arg in "$@"; do
esac esac
done done
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="${CM_ROOT:-$HOME/.claude-mode}" ROOT="${CM_ROOT:-$HOME/.claude-mode}"
BINDIR="$HOME/.local/bin" BINDIR="$HOME/.local/bin"
@@ -28,6 +27,21 @@ green() { printf ' \033[32mok \033[0m %s\n' "$*"; }
warn() { printf ' \033[33mwarn\033[0m %s\n' "$*"; } warn() { printf ' \033[33mwarn\033[0m %s\n' "$*"; }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; } fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; }
# A checkout next to this script is the normal source. Piped in
# (curl ... | bash) there is no script file and no checkout, so fetch the
# repository archive and install from that instead.
SRC="$(cd "$(dirname "${BASH_SOURCE[0]:-''}")" 2>/dev/null && pwd)" || SRC=""
if [ ! -f "$SRC/claude-mode" ] || [ ! -f "$SRC/cm-json.py" ]; then
REPO_URL="${CM_REPO_URL:-https://git.nebulm.com/smoido/claude-mode}"
command -v curl >/dev/null 2>&1 || { fail 'curl required to fetch the source'; exit 1; }
TMP="$(mktemp -d "${TMPDIR:-/tmp}/claude-mode-src.XXXXXX")"
printf 'no local checkout found - fetching source from %s\n' "$REPO_URL"
curl -fsSL "$REPO_URL/archive/master.tar.gz" | tar -xz -C "$TMP"
EXTRACTED="$(find "$TMP" -type f -name claude-mode -print -quit)"
[ -n "$EXTRACTED" ] || { fail 'archive did not contain linux/claude-mode - repo layout changed?'; exit 1; }
SRC="$(dirname "$EXTRACTED")"
fi
printf '\ninstalling claude-mode -> %s\n' "$ROOT" printf '\ninstalling claude-mode -> %s\n' "$ROOT"
# --- preflight ------------------------------------------------------------- # --- preflight -------------------------------------------------------------