export ANTHROPIC_BASE_URL=http://127.0.0.1:20128
in your shell routes the claude
CLI to a local endpoint. Open the Claude Code panel in VSCode in the same project and it still talks to Anthropic directly. The two live on different environment surfaces, and that is the whole problem.
The CLI reads the environment of the process you started it from. The VSCode panel does not: the extension spawns its agent process from the VSCode window process, so its environment is whatever VSCode itself was started with — the desktop/login environment, not the shell you typed export
into five seconds ago.
Two consequences that bite in opposite directions:
The escape hatch the extension gives you is a settings key, claudeCode.environmentVariables
, which is a list of {name, value}
pairs injected into the agent process. You can check what your panel actually sees right now — this runs anywhere jq
does:
S="$HOME/Library/Application Support/Code/User/settings.json"
jq -r '.["claudeCode.environmentVariables"] // []
| if length == 0
then "panel env: (none set) -> extension inherits the VSCode process env"
else (.[] | "\(.name)=\(if .name|test("TOKEN|KEY") then "***" else .value end)")
end' "$S"
On a machine that has never been switched, that prints the (none set)
line. That is the honest answer to "why is my panel ignoring the variable".
claudeCode.environmentVariables
is a plain array in your user settings.json
, and hand-editing it has three failure modes I hit before writing anything:
ANTHROPIC_BASE_URL
behind while removing the token and the panel points at a router with no credential.NODE_EXTRA_CA_CERTS
— is one careless overwrite away from gone.So the toolkit I ended up with treats the array as partly owned. It manages exactly six names:
ANTHROPIC_BASE_URL
ANTHROPIC_AUTH_TOKEN
ANTHROPIC_API_KEY
ANTHROPIC_MODEL
ANTHROPIC_SMALL_FAST_MODEL
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
Entries whose name is not on that list are read, kept aside, and written back untouched. Before any write it copies settings.json
to settings.json.bak-claude-router
, and if serialisation throws it restores the backup and exits non-zero rather than leaving you with half a settings file.
& "$env:USERPROFILE\.claude\9router\vscode-switch.ps1" on # inject the six
& "$env:USERPROFILE\.claude\9router\vscode-switch.ps1" status # print base URL + model, never the token
& "$env:USERPROFILE\.claude\9router\vscode-switch.ps1" off # remove only the six
on
and off
need a Developer: Reload Window afterwards — the agent process reads its environment once at spawn.
For the CLI the right scope is the narrow one, so there routing is per-process and nothing is persisted:
claude # default Anthropic connection
claude-9router # same CLI, routed
claude-9router --resume # normal Claude CLI arguments pass through
claude-9router
sets the variables inside its own process, unsets ANTHROPIC_API_KEY
so a leftover key cannot win over the router token, then execs claude
. Close the window and the routing is gone. Nothing in ~/.claude
is rewritten.
That asymmetry is deliberate and it is the actual design decision: the terminal gets process scope because a shell is cheap to start, the panel gets machine scope with an explicit on/off because the extension gives no narrower hook. Cost of the second: you cannot have one routed window and one direct window at the same time. If you need both, use the panel for one and claude-9router
in a terminal for the other.
The endpoint is a 9Router instance, whose Anthropic-compatible /v1/messages
fronts a long list of providers. The toolkit hard-codes none of them; it reads a gitignored local file:
{
"baseUrl": "http://127.0.0.1:20128",
"authToken": "your-9router-api-key",
"mainModel": "provider/model-id",
"smallFastModel": "provider/smaller-model-id"
}
Limits, stated plainly:
Code, tests and setup guide: ** https://github.com/vinhnguyenthanhdn/claude-router** (MIT).
The open gap is the platform one: the Windows paths are the only ones implemented and tested, and porting is work I have not done. Three issues are scoped and open:
claude-9router.ps1
If you only want to confirm the diagnosis rather than write code: run the jq
snippet above on your machine and tell me what your panel is actually inheriting. That answer is useful on its own.