A per-account CLI that toggles Claude Code between backends by editing exactly
one file: ~/.claude/settings.json.
It never touches settings.local.json, .credentials.json, or .claude.json
- those are regenerated by other tooling (team config sync, the
claudeCLI itself). Becauseclaude-modeonly ownssettings.json, a switch survives a team-wide config re-sync.
Every site-specific value - proxy URLs, ports, model IDs, tunnel host, token
env var names, the toggle order - lives in a profile.json you install
locally. Nothing in bin/ or lib/ hardcodes a particular site.
Four backends, one command:
| Mode | What it routes to |
|---|---|
oauth |
Your Claude subscription (OAuth login) |
nexus-claude |
Claude models via your site's Bedrock-compatible proxy |
nexus-gpt |
GPT-tier Claude Code model IDs via the same proxy |
local |
An on-prem model, via the same proxy or direct (local.transport) |
See docs/modes.md for exactly what each mode writes to settings.json and
why.
git clone <this-repo> claude-mode
cd claude-mode
./install.shinstall.sh is idempotent:
- puts everything in
bin/into~/.local/bin, pointing back at this checkout, sogit pullis the only update step you ever need. On macOS, Linux and WSL that's a symlink; on Windows see below. Anything already at the destination that install.sh didn't create is left alone with a warning. - writes an initial
profile.jsonfromprofiles/example.json- to~/.claude/claude-mode/profile.jsonwhen~/.claudeexists, else to~/.config/claude-mode/profile.json- only if no profile exists in either location - it never clobbers a profile you've already configured - warns (does not fail) if
~/.local/binisn't on yourPATH
The starter profile is a template, not a working config - its model IDs
and URLs are placeholders. Edit it before your first claude-mode run; see
Profile setup.
Use --dry-run to preview every action without writing anything:
./install.sh --dry-rununinstall.sh removes only what install.sh created (it verifies each entry
still points into this repo before removing it) and never touches
profile.json or ~/.claude/settings.json.
Run install.sh from Git Bash or MSYS2 - it needs a POSIX shell and
cygpath, both of which Git Bash ships. It detects native Windows and adapts:
- instead of symlinks (Git Bash's
ln -ssilently copies, which would freeze your install at the current commit) it writes a launcher pair per script -claude-modefor Git Bash andclaude-mode.cmdfor PowerShell/cmd - each exec'ing the script out of this checkout - it resolves
~the way the Python CLI does (%USERPROFILE%), not from$HOME, which Git Bash often points at a network share - soprofile.jsonlands whereclaude-modewill actually read it
WSL is not native Windows: there everything installs as a normal POSIX environment, symlinks and all.
Edit your profile.json after install. It is looked for in this order:
$CLAUDE_MODE_PROFILE(explicit file path override, always wins)~/.claude/claude-mode/profile.json, if it exists (preferred - beside thesettings.jsonthis tool manages)$XDG_CONFIG_HOME/claude-mode/profile.jsonor~/.config/claude-mode/profile.json(legacy fallback)
Start from profiles/example.json's shape:
{
"bedrock": { "base_url": "http://localhost:8104", "port": 8104 },
"local": { "base_url": "http://localhost:8901", "port": 8901,
"transport": "bedrock" },
"models": {
"oauth": { "opus": "...", "sonnet": "...", "haiku": "...", "small_fast": "..." },
"nexus": { "opus": "...", "sonnet": "...", "haiku": "...", "small_fast": "..." },
"gpt": { "opus": "...", "sonnet": "...", "haiku": "...", "small_fast": "..." },
"local": { "opus": "...", "sonnet": "...", "haiku": "...", "small_fast": "..." }
},
"claude_model": { "oauth": null, "nexus": null, "gpt": null, "local": null },
"tokens": {
"bedrock_env": "AWS_BEARER_TOKEN_BEDROCK",
"env_file": null
},
"test": { "timeout_seconds": 20 },
"tunnel": { "host": "your-server.example.com", "user": null, "jump": null },
"toggle_cycle": ["oauth", "nexus", "gpt", "local"]
}claude_model.<mode> optionally pins the top-level model key in
settings.json per mode - useful when a model alias set there is only
valid on one backend. null or omitted (the default) means "leave the
user's model key alone". Leaving a pinned mode for an unpinned one
undoes the pin (restoring your own pre-pin model value, if you had one)
- see
docs/modes.mdfor the exact semantics.
local.transport picks how local mode reaches the on-prem model:
"bedrock"(the default, and the only behavior before this key existed) routes it through the Bedrock-compatible proxy, exactly like thenexusmodes. Profiles written before this key keep working with no edit."direct"pointsANTHROPIC_BASE_URLstraight atlocal.base_url, for a server that already speaks the Anthropic Messages API natively - no proxy and no Bedrock emulation in between.
Ollama serves that API at /v1/messages, so pointing local at a machine's
own Ollama is just:
"local": { "base_url": "http://localhost:11434", "transport": "direct" },
"models": { "local": { "opus": "llama3.2:1b", "sonnet": "llama3.2:1b",
"haiku": "llama3.2:1b", "small_fast": "llama3.2:1b" } }No token is needed on that path, and no tunnel - claude-mode local gates on
the local endpoint itself rather than on the proxy. Switching to any other
mode tears the direct keys back down, so a stale ANTHROPIC_BASE_URL can
never leave a later backend pointed at the local server.
Any key you omit falls back to a generic built-in default (see
lib/profile.py's DEFAULTS); a key with no sane generic default (a real
model ID, a real tunnel host) raises a clear ABORT: missing required profile key: '...' naming exactly what's missing, instead of silently using
a placeholder.
toggle_cycle entries may use the internal short names (nexus, gpt) or
the canonical command names (nexus-claude, nexus-gpt) - an entry that
matches no known mode makes claude-mode toggle abort instead of guessing.
$CLAUDE_MODE_PROFILE overrides the profile file path entirely - useful for
tests, or running two profiles side by side.
Tokens (Nexus gateway token, etc.) are never stored in profile.json. They
are read fresh on every switch from the first env file that contains the
key: ~/.claude/.env, then ~/.env - or exactly the file named by
tokens.env_file when that profile key is set (see docs/modes.md).
claude-mode status # show active backend, proxy health, and account
claude-mode nexus-claude # Claude models through the configured proxy
claude-mode nexus-gpt # GPT-tier models through the same proxy
claude-mode local # route to the on-prem model (proxied or direct)
claude-mode oauth # route back to the Claude subscription
claude-mode toggle # cycle through profile.json's toggle_cycle
claude-mode test # live round-trip through the active backend
Aliases: st for status; nexus/bedrock for nexus-claude;
gpt/sol/luna/terra for nexus-gpt; claude/sub/subscription for
oauth.
nexus-claude and nexus-gpt are both Claude Code modes using the same
Bedrock-compatible Nexus proxy. They read the same token variable,
tokens.bedrock_env (normally AWS_BEARER_TOKEN_BEDROCK), from
~/.claude/.env first, then ~/.env. The GPT mode changes only the model
IDs; it does not launch Codex or another CLI.
Restart your claude session (exit and relaunch) after switching - the CLI
reads its environment once at startup.
Two extra launcher wrappers ship in bin/:
cc- launchesclaudepinned to theoauthmodel tier regardless of whatever else is currently configured in your shell environment.cc-gpt- launchesclaudepinned to thenexus-gpttier using the same shared Nexus token asnexus-claude.
If your machine can't reach the proxy directly (a laptop off the plant
network, a personal machine), claude-mode-tunnel opens an SSH tunnel first.
See docs/remote-setup.md for the full walkthrough.
claude-mode-tunnel up # start the tunnel (no-op if already running)
claude-mode-tunnel status # pid + per-port reachability
claude-mode-tunnel down # stop it
ABORT: no <token> found in ...- add the token env var named in the error message to one of the files the message lists (KEY=valueform, one per line;~/.claude/.envis searched first, then~/.env). That file should bechmod 600and is git-ignored by design;claude-modeonly ever reads it, never writes it. The file must be UTF-8 (a UTF-8 BOM is tolerated) - Windows PowerShell 5.1'secho 'KEY=...' >> filewrites UTF-16, whichclaude-modewarns about and skips; useAdd-Content -Encoding utf8or a UTF-8 editor instead.ABORT: <proxy> is not answering- nothing answered HTTP at the URL your profile points at (an error status like 401/404 counts as answering, so this really means down or unreachable). Iftunnel.hostis set in your profile, the error tells you to runclaude-mode-tunnel upfirst.ABORT: missing required profile key: '...'- that dotted key has no generic default and isn't set in yourprofile.json. Add it.claude-mode testtimes out on a slow machine - raisetest.timeout_secondsin yourprofile.json(default 20).- Switch reports success but Claude Code still behaves like the old
mode - you didn't restart the session. Exit and relaunch
claude. claude-modenot found -~/.local/binisn't on yourPATH. Addexport PATH="$HOME/.local/bin:$PATH"to your shell rc file.- Windows:
claude-modenot found in PowerShell/cmd - runinstall.shfrom Git Bash (see Windows under Install); it generates the.cmdlaunchers PowerShell needs.~/.local/binstill has to be on your WindowsPATH. - Windows:
python3"not found" / Microsoft Store nag - that's the Store app-execution alias shadowing a real interpreter. Every entry point probespython3 --versionand falls back topython, so a workingpythonis enough; if neither runs, install Python 3 or turn the alias off under Settings > Apps > Advanced app settings > App execution aliases. - Windows - the Python CLI itself is OS-agnostic; under WSL everything
runs unmodified as a normal POSIX environment.
On native Windows,
claude-moderesolves the same home directory theclaudeCLI does (%USERPROFILE%) and deliberately ignores a strayHOMEenv var (Cygwin/MobaXterm/Emacs setups often set one) - otherwise it would edit asettings.jsonthe CLI never reads. SetCLAUDE_MODE_HOMEto force a specific home directory (the test suite uses this).
bin/ the installed commands (claude-mode, claude-mode-tunnel, cc, cc-gpt)
lib/ shared Python modules: profile loading, settings.json I/O, platform detection
plus python.sh, the interpreter probe every shell entry point sources
profiles/ profiles/example.json - the template install.sh copies from
tests/ unittest suite (`make test`)
install.sh / uninstall.sh
Makefile `make check` = syntax check + compile + unittest + secret/site-value scrub
MIT - see LICENSE.