setup · git:20260917.6aa7e90 · 2026-09-17 · sha256 9fcf33f6d0c5e8fd

setup git:20260917.6aa7e90C

Immutable. This exact content is served forever at /api/v1/blob/9fcf33f6d0c5e8fd.

---
name: setup
description: One-time setup for the cc-proxy plugin. Writes API keys (all optional — GLM_API_KEY, OPENROUTER_API_KEY, DEEPSEEK_API_KEY, DASHSCOPE_API_KEY, LMSTUDIO_BASE_URL, LMSTUDIO_API_KEY) to ~/.env, and configures ANTHROPIC_BASE_URL plus a /model picker row per routable model (each carrying its real context window) in ~/.claude/settings.json, so the SessionStart hook can auto-start the proxy and /model can route to every configured backend. Invoke via /cc-proxy:setup.
---

# cc-proxy setup

One-time configuration of `~/.claude/settings.json` so the proxy runs automatically on every Claude Code session.

The proxy binary needs **no configuration**: the SessionStart hook and `scripts/start-proxy.js` resolve `bin/cc-proxy.js` from their own plugin tree, which is always the currently-installed version. Do **not** write `PROXY_PATH` into settings.json — a version-pinned path there is exactly how users used to get stuck on stale proxies after plugin updates. If a `PROXY_PATH` already exists in settings.json `env`, **remove it** during step 3 (it is a legacy pin; the tree's own bin outranks it anyway).

## What to do

Follow these steps **exactly**. Do not skip any.

### 1. Locate the plugin tree (for the statusline path only)

Check these locations in order and use the first one that exists:

1. `~/.claude/plugins/cache/betmoar/cc-proxy/*/` (marketplace install — the normal case; glob the `*` version segment and take the newest if there is more than one)
2. `~/dev/cc-proxy-plugin/` (dev-repo fallback, if the user cloned source)

This concrete path is needed **only** for the optional statusline command in step 4 (which runs outside plugin context). It is *not* written as `PROXY_PATH`.

### 2. Collect provider API keys (written to `~/.env`)

API keys live in `~/.env` — the single source of truth the proxy reads at startup. Do **not** put keys in `~/.claude/settings.json` `env`; it keeps only non-secret plumbing (step 3).

Read `~/.env` first (create the file if absent). For each key, reuse a value already present rather than re-asking.

**Z.ai / GLM — optional, but the one this plugin is built around.** Its models are the bulk of what step 3b publishes to the `/model` picker; without the key none of them appear there. If `GLM_API_KEY` is missing or empty in `~/.env`, **ask explicitly**:

> "Enter your Z.ai API key (https://z.ai → Dashboard → API Keys), or press Enter to skip. It will be stored in ~/.env:"

If the user skips, continue setup — do not stop and do not re-ask. The proxy
starts without it and routes to Claude; a backend with no key is simply not
registered. Say once that no GLM models will appear in `/model` until a key is
added, then move on.

**OpenRouter — optional.** Ask the user whether they also want OpenRouter routing. If yes and `OPENROUTER_API_KEY` is missing or empty in `~/.env`, ask:

> "Enter your OpenRouter API key (https://openrouter.ai/settings/keys). It will be stored in ~/.env:"

**DeepSeek — optional.** Ask the user whether they also want DeepSeek routing. If yes and `DEEPSEEK_API_KEY` is missing or empty in `~/.env`, ask:

> "Enter your DeepSeek API key (https://platform.deepseek.com/api_keys). It will be stored in ~/.env:"

**Qwen — optional.** Ask the user whether they also want Qwen (QwenCloud Token Plan) routing. If yes and `DASHSCOPE_API_KEY` is missing or empty in `~/.env`, ask:

> "Enter your QwenCloud Token Plan API key (ANTHROPIC_AUTH_TOKEN from the QwenCloud console). It will be stored in ~/.env:"

**LM Studio — optional (self-hosted).** Ask the user whether they also run an LM Studio server they want reachable. If yes and `LMSTUDIO_BASE_URL` is missing or empty in `~/.env`, ask:

> "Enter your LM Studio server base URL, INCLUDING the http:// scheme (e.g. http://192.168.1.50:1234 — LM Studio's own UI shows the address without it, and a scheme-less value is refused. The server must also have 'Serve on Local Network' enabled to be reachable from another machine). It will be stored in ~/.env:"

If the user's server has "Require Authentication" enabled and `LMSTUDIO_API_KEY` is missing or empty, also ask:

> "Enter your LM Studio API key. It will be stored in ~/.env:"

(Auth-off servers need no key; a dummy token is sent and ignored. `LMSTUDIO_BASE_URL` is the opt-in — the key alone registers nothing.)

Write each collected key to `~/.env` as a `KEY=value` line, one per line (e.g. `GLM_API_KEY=<value>`). If `~/.env` already exists, **merge** — update only the key lines you collected and preserve every other line unchanged. If it does not exist, create it with just the key line(s).

The proxy only registers OpenRouter when `OPENROUTER_API_KEY` is set, and routes any model id containing a slash to it (e.g. `z-ai/glm-4.7`, `deepseek/deepseek-v4-pro` — never an `anthropic/…` id: that routes Claude traffic through a metered reseller, which invariants 3 and 4 exist to prevent, and discovery deliberately omits those ids). It only registers DeepSeek when `DEEPSEEK_API_KEY` is set, and routes any bare `deepseek-*` id to it (e.g. `deepseek-v4-pro`, `deepseek-v4-flash`). It only registers Qwen when `DASHSCOPE_API_KEY` is set, and routes any bare `qwen`-prefixed id to it (e.g. `qwen3.7-max`, `qwen3.6-flash`). It only registers LM Studio when `LMSTUDIO_BASE_URL` is set, and routes **only** explicit `lmstudio:<model-id>` selectors to it — no bare id routes there by shape (local model names would collide with the GLM/Qwen/OpenRouter predicates above); `DEFAULT_BACKEND=lmstudio` additionally makes it the unmatched-id fallback, which is an explicit user choice like `=openrouter`. **Tell the user what does and does not reach the `/model` picker:** step 3b publishes a picker row for every model with a curated context window whose provider has a key — GLM, DeepSeek and Qwen ids all appear. Two kinds do **not**: OpenRouter's `vendor/model` ids (~400 of them, no curated window each) and LM Studio's, whose names are per-machine and arbitrary. Those are reached by (a) `DEFAULT_BACKEND=openrouter` (or `lmstudio`) so unmatched requests fall through, (b) a subagent/slash-command whose frontmatter pins the id verbatim, or — for LM Studio — (c) a `/model lmstudio:<model-id>` selector, since bare local ids cannot route by shape anyway.

**Migrate existing keys (one source of truth).** Read `~/.claude/settings.json`. If its `env` block contains `GLM_API_KEY`, `OPENROUTER_API_KEY`, `DEEPSEEK_API_KEY`, `DASHSCOPE_API_KEY`, `LMSTUDIO_BASE_URL`, or `LMSTUDIO_API_KEY` (legacy setups), move them to `~/.env`: if `~/.env` already has the key, keep the `~/.env` value and just drop the settings.json copy; otherwise copy the value over then **remove** the key from settings.json `env`. After setup, keys must exist **only** in `~/.env`.

### 3. Update `~/.claude/settings.json` (plumbing only — no keys)

Read the current file, then merge the following into the `env` object (create `env` if missing). Preserve every other existing key unchanged, **except `PROXY_PATH`: delete it if present** (legacy version-pinned path; the hook resolves the binary from its own tree now). **Do not add `GLM_API_KEY`, `OPENROUTER_API_KEY`, `DEEPSEEK_API_KEY`, `DASHSCOPE_API_KEY`, `LMSTUDIO_BASE_URL`, or `LMSTUDIO_API_KEY` here** — they go in `~/.env` (step 2).

```json
{
  "env": {
    "ANTHROPIC_BASE_URL": "http://127.0.0.1:4000"
  }
}
```

Write the file back with 2-space indentation, matching the existing formatting.

Do **not** write `ANTHROPIC_CUSTOM_MODEL_OPTION` here — step 3b replaces it with
a full row set. Claude Code dedupes the picker by model id and that env WINS, so
leaving it in place replaces the generated row for that model with a bare one —
losing its `behavesAs` (the catalog warning returns for that id) and showing
"Custom model" instead of its context window.

**PROXY_AUTH_TOKEN — only if the user says the proxy must be reachable off this
machine** (e.g. another device on the LAN, `PROXY_HOST=0.0.0.0`). Ask:

> "You want the proxy reachable from other machines. It injects API keys, so it needs a shared token. Enter one (any long random string), or press Enter to skip and keep loopback-only:"

If given: write `PROXY_AUTH_TOKEN=<value>` to `~/.env` **and** merge
`"ANTHROPIC_AUTH_TOKEN": "<same value>"` into settings.json `env` (that is how
Claude Code presents it to the proxy). Also warn, once, in plain words: with the
token in place, Claude Code's single credential slot is the proxy token, so
`claude-*` model requests no longer carry OAuth — this mode is for GLM/OpenRouter/
DeepSeek/Qwen/LM Studio routing. If the user did not ask for off-host access, do
NOT configure any of this; the loopback default needs no token.

### 3b. Publish the `/model` picker rows

Run, verbatim:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/render-model-picker.js"
```

**Do not write the `modelPicker` block yourself.** Claude Code does no merging
across settings sources — the highest-precedence file that defines `modelPicker`
wins outright — so a hand-written block silently discards any rows the user
added themselves, with no diagnostic anywhere. The script merges: it replaces
only the rows cc-proxy previously generated, keeps foreign rows in their exact
positions, backs the file up to `settings.json.bak` first, and removes the
superseded `ANTHROPIC_CUSTOM_MODEL_OPTION*` keys. The write is atomic (temp file
renamed over the target), so an interrupted run cannot leave a half-written
settings.json.

**Why this exists.** Claude Code assumes a **200K** context window for any model
id its built-in catalog does not describe — which is every id cc-proxy routes —
and auto-compacts there no matter what the backend actually serves. The rows
carry each model's real window (via a `[1m]` suffix on the ids that have one) and
a `behavesAs` mapping that stops the "isn't described by this version's model
catalog" warning.

Interpret the script's output:

- `wrote N modelPicker rows to …` → success. It also prints which env keys it
  removed. Continue.
- `no provider keys are registered` → the user skipped every key in step 2, so
  there is nothing to publish and nothing was written. Say so plainly: the
  `/model` picker keeps only Claude's built-in entries, and re-running
  `/cc-proxy:setup` after adding a key to `~/.env` will add the rows.
- `could not be read (…)` → their settings.json could not be read or parsed;
  the parenthetical says which (a JSON error, EACCES, EISDIR). Nothing was
  written. Relay the reason and stop; do not attempt a repair.

**If the script reports `CLAUDE_CODE_MAX_CONTEXT_TOKENS is still set`, ask the
user** — do not remove it yourself:

> "Your settings pin `CLAUDE_CODE_MAX_CONTEXT_TOKENS` to `<value>` globally. Each model now carries its own window, so that pin no longer affects them — it only applies to ids with **no** row (a typo, a model pinned in a subagent's frontmatter, an OpenRouter `vendor/model` id), where a large value tells Claude Code not to compact until far past what those models actually accept. Remove it?"

If they say yes, delete just that key from settings.json `env` and leave
everything else untouched. If they say no or don't answer, leave it — it is
harmless for every model that has a row.

**Tell the user this limitation once:** models whose real window is *below* 200K
(`glm-4.5`, `glm-4.5-air` at 128K) are still budgeted at 200K. Claude Code has no
per-model channel below 200K, and the global one is disabled for any model
carrying `behavesAs`.

### 4. Optional: enable the statusline

Ask the user whether they want the quota/credits statusline. It shows Claude 5-hour usage, GLM coding quota, OpenRouter credits (when `OPENROUTER_API_KEY` is set), DeepSeek balance (when `DEEPSEEK_API_KEY` is set), Qwen presence (when `DASHSCOPE_API_KEY` is set), and a bold-red `proxy down` when the local proxy is unreachable.

If yes, merge this **top-level** key into `~/.claude/settings.json` (it is *not* under `env`):

```json
{
  "statusLine": {
    "type": "command",
    "command": "node <PROXY_DIR>/scripts/statusline.js"
  }
}
```

`<PROXY_DIR>` is the plugin tree located in step 1 (e.g. `~/.claude/plugins/cache/betmoar/cc-proxy/<version>`). The statusline command runs outside plugin context, so `${CLAUDE_PLUGIN_ROOT}` is unavailable — an absolute path is required here, and it *is* version-pinned (a statusline pointing at an older cache dir still renders; it does not affect which proxy runs). If the user already has a `statusLine` configured, show them the command and let them decide rather than overwriting it.

### 5. Start the proxy now

Spawn the proxy so it is already up when `ANTHROPIC_BASE_URL` takes effect, eliminating the first-run `ECONNREFUSED`. Run, verbatim:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/start-proxy.js"
```

`scripts/start-proxy.js` reuses the SessionStart hook's `ensureProxyRunning()`: it probes `PROXY_PORT` first (idempotent — a same-version proxy is left running; a stale-version one is gracefully replaced), then spawns its own tree's `bin/cc-proxy.js` detached + `unref`'d so it survives this turn. It reads the `env` block you just wrote to `~/.claude/settings.json` and passes it to the spawn, because the proxy reads config from env (not settings.json) and nothing has injected those vars into this process yet on a first-run setup.

Interpret the script's stdout/stderr:

- `cc-proxy already up`, `cc-proxy started`, or `cc-proxy restarted` → success. Proceed to step 6.
- `PROXY_PATH is unset` → the plugin tree has no `bin/cc-proxy.js` (hand-rolled install) and no legacy `PROXY_PATH` exists. Ask the user where `cc-proxy.js` is and put that absolute path in settings.json `env` as `PROXY_PATH` — the one case where it is still legitimate.
- `did not become reachable in time` → spawn fired but readiness timed out. Treat as a fallback: keep `/exit` + `/resume` as the path to recovery (step 6 covers this). Show the user the `~/.claude/cc-proxy/cc-proxy.log` tail if they ask.

### 6. Inform the user

Tell the user, verbatim:

> Setup complete. The proxy is running (step 5 started it). Claude Code re-applies `ANTHROPIC_BASE_URL` to running sessions immediately, so any open `claude` may still fail until it re-reads env — `/exit` and `/resume` any open session if you hit an error.
>
> To confirm, check `~/.claude/cc-proxy/cc-proxy.log` after your next prompt — you should see routing lines like `claude-sonnet-4-6 -> claude` or `glm-5.3 -> glm`.

## Important constraints

- **Do not** overwrite unrelated keys in `settings.json` or unrelated lines in `~/.env`. Use a merge strategy for both, not a full rewrite from template.
- **Do not** commit the user's API key anywhere. API keys stay only in `~/.env` (it is gitignored). They must **not** appear in `~/.claude/settings.json`.
- **Do not** start the proxy by hand with `node bin/cc-proxy.js` or similar — use `scripts/start-proxy.js` (step 5), which is idempotent and passes settings.json's plumbing env to the spawn. Raw starts risk duplicate proxies on the port.
- If `~/.claude/settings.json` does not exist, create it with just the `env` block above (and valid JSON structure).