Keep your Claude Code main conversation on your claude.ai subscription while routing subagent traffic to a third-party provider you pay for separately.
English · 繁體中文
ultracode and Workflow fan out dozens of subagents at once, and a claude.ai subscription's 5-hour limit does not survive that. The subagents are the bulk of the tokens but not the part where reasoning quality matters most, so this router sends them to a cheaper provider and leaves the main conversation on the subscription, untouched.
Warning
Read this before using it.
- Unofficial. Not affiliated with, endorsed by, or sponsored by Anthropic PBC. Anthropic explicitly does not support pointing Claude Code at non-Claude models. If it breaks, you fix it.
- Your data goes to the third party in full. Every routed request carries the whole payload — system prompt, your source code, file contents, tool output. Route subagents somewhere you would be comfortable sending your repository.
- Your claude.ai OAuth token passes through this local proxy. It is forwarded to Anthropic unchanged and is never sent to a third-party provider (the code that guarantees it, and the tests that pin it).
- Third-party usage is billed to your own API key. This tool does not modify or spoof any billing identity, and does not bypass anyone's usage limits. Check it against your terms with each provider. Use at your own risk.
From Claude Code's own LLM gateway docs:
Setting only that variable (
ANTHROPIC_BASE_URL), without a gateway credential, doesn't replace the subscription. Requests still route through the gateway, but a saved claude.ai login remains the active credential, so its usage limits and billing apply.
So as long as you do not set ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, or
apiKeyHelper, Claude Code sends its subscription OAuth token to this router,
and the router decides per request whether it goes to Anthropic (subscription
pays) or to a third party (your API key pays).
The split is made on a header from the gateway protocol:
x-claude-code-agent-id— Identifier of the subagent that issued the request, present only on requests from an agent Claude Code spawned inside the session.
Matching on the header rather than the model name matters because Workflow's
agent() only accepts the sonnet | opus | haiku | fable aliases — you cannot
name a third-party model inside a workflow script at all.
Node 20+. No npm dependencies.
git clone https://github.com/1morr/subagent-handoff.git
cd subagent-handoff
npm startThe first run creates config.json. Nothing is routed out of the box — the
default "all subagents" rule ships disabled, because with no provider key yet,
enabling it would just 401 every subagent.
Open http://127.0.0.1:8788:
- Providers — enter Base URL, API Key and Model, click Run test, and confirm every Required check passes. A failing Capability check does not stop Claude Code from working, but that capability silently fails for subagents; see the built-in tests. Click Save (top right).
- Routing — tick the "All subagents (including Workflow / ultracode)" rule, make sure it points at your provider, then click Save.
- Connect — copy the snippet into
~/.claude/settings.jsonand restart Claude Code. A per-repo<repo>/.claude/settings.local.jsonenvblock works too, if you only want one project routed. - Run
/statusand confirmLogin methodstill points at your claude.ai account. - Give a subagent some work, then watch the split on the Rack tab.
Keep the router running while Claude Code runs: when it is down, Claude Code's
API requests fail. To stop using it, remove ANTHROPIC_BASE_URL from Claude
Code's settings and restart Claude Code. Using Claude Desktop? It ignores
ANTHROPIC_BASE_URL; read HTTPS proxy mode.
Off by default, and while it is off the router behaves exactly as described above. Turn it on only if you want Claude Desktop routed too.
Why: Claude Desktop's Code tab ignores ANTHROPIC_BASE_URL, so it never
reaches the router. It does honour HTTPS_PROXY and NODE_EXTRA_CA_CERTS.
How: off, Claude Code sends its API requests to the router. On, Claude Code thinks it talks to Anthropic directly, and the router picks the connection up as an HTTPS proxy:
flowchart LR
CC["Claude Code<br/>CLI or Desktop"] -- HTTPS_PROXY --> R{router}
R -- "api.anthropic.com<br/>decrypted with a local CA" --> S{path}
S -- "/v1/messages*, main" --> A[(Anthropic<br/>subscription)]
S -- "/v1/messages*, subagent" --> P[(your provider)]
S -- "other paths, WebSocket<br/>forwarded untouched" --> A
R -- "any other host<br/>plain tunnel" --> N((internet))
The /v1/messages* routing is the same code in both modes; only the way requests
get in differs.
| Off (default) | On | |
|---|---|---|
| Clients | CLI | CLI and Claude Desktop |
| Claude Code settings | ANTHROPIC_BASE_URL |
unchanged for the CLI; Desktop (or a CLI you switch over) uses HTTPS_PROXY + NODE_EXTRA_CA_CERTS |
| Through the router | API requests only | every HTTPS connection of Claude Code and the programs it starts; only api.anthropic.com is decrypted |
| Router down | API requests fail | clients set to HTTPS_PROXY, plus their git, npm…, lose all network access |
| Local CA | none | one, name-constrained to api.anthropic.com |
Turning it on does not change the CLI setup. The mode only adds an entrance
(CONNECT); requests sent to ANTHROPIC_BASE_URL come in exactly as before, so a
CLI using it keeps working without any change. The mode is for clients that cannot
be pointed at the router with ANTHROPIC_BASE_URL: Claude Desktop, and the CLI's
Remote Control. If you only use the CLI, you do not need it.
To turn it on: press Turn on and save in the HTTPS proxy mode panel at the
bottom of the Connect tab. The router switches on the spot, no restart. The panel
then shows the HTTPS_PROXY + NODE_EXTRA_CA_CERTS snippet for Desktop and where to
put it:
| Your setup | Where the settings go |
|---|---|
| CLI only | Keep ANTHROPIC_BASE_URL; the mode is not needed |
| CLI and Desktop both through the router | The snippet in the global ~/.claude/settings.json, with ANTHROPIC_BASE_URL removed there; the CLI then also goes through the proxy |
| Desktop in one repo only | The snippet in that repo's .claude/settings.local.json, plus "ANTHROPIC_BASE_URL": "https://api.anthropic.com" to override the global one |
One set of settings must not hold both HTTPS_PROXY and an http://
ANTHROPIC_BASE_URL — project settings add to the global ones. The CLI then treats
the router as an ordinary proxy and the router answers every request with a 400
naming the two settings. Hand-editing "httpsProxy" in config.json only takes
effect when the router restarts.
To turn it off: press Turn off and save. A CLI using ANTHROPIC_BASE_URL is
not affected. Anything still set to HTTPS_PROXY has no network at all once the mode
is off: remove HTTPS_PROXY and NODE_EXTRA_CA_CERTS from its settings and restart it.
Account risk: nobody can promise it is safe. In both modes the subscription
requests keep your token and body but leave from the router, with Node's TLS
fingerprint and two headers Node's fetch adds. With the mode on, sign-in,
telemetry, Remote Control and voice also leave from Node. Measured details, the
full diagrams, global vs per-repo setup and the risk write-up:
docs/https-proxy.md.
| Condition | How it is detected | Who it is |
|---|---|---|
main |
No x-claude-code-agent-id |
You typing in the prompt box |
subagent |
Has x-claude-code-agent-id |
Any agent Claude Code spawned. Workflow and ultracode agent() calls are all here, and so are agents a subagent spawns in turn |
Background requests without an agent id count as main and follow the main
rules: session titles, compaction, the quota probe, and the auto mode permission
classifier — even when it is judging a subagent's action. Route main to a
provider and auto mode is judged by that provider's model, not by Claude. Every
request type and where it goes, measured: docs/request-map.md.
The GUI is a complete front end for config.json; everything except the ports is
editable there. Rules are evaluated top to bottom and the first match wins.
| Field | |
|---|---|
proxyPort / adminPort |
8787 and 8788. Not in the GUI: edit config.json and restart. Everything else takes effect as soon as it is saved in the GUI |
httpsProxy |
HTTPS proxy mode for Claude Desktop, default false. Saving it in the GUI switches the router immediately |
providers[].baseUrl |
Must speak the Anthropic Messages format — the router posts to {baseUrl}/v1/messages |
providers[].model |
Rewrites model before sending. Empty = leave alone |
providers[].authStyle |
bearer or x-api-key |
rules[].match |
main or subagent, optionally narrowed by modelGlob |
rules[].providerId |
Which provider, or the reserved value passthrough to send it back to the subscription |
rules[].modelOverride |
Rewrites model, beating providers[].model. Works on passthrough too |
Unmatched requests go to https://api.anthropic.com with their credentials
unchanged; that target is fixed.
The router reads config.json once, at startup. Hand edits made while it runs
are ignored, and the next GUI save overwrites them — stop the router, edit,
start it again. The ROUTER_CONFIG environment variable moves the file, and
traffic.log and the HTTPS proxy mode CA move with it (so
NODE_EXTRA_CA_CERTS must follow). Full reference, including what an older
config.json loses on its next save: docs/configuration.md.
Two things worth knowing. When a third-party quota runs dry, switch that
rule's target to passthrough rather than disabling it — disabling drops traffic
through to the next rule, while pointing at passthrough actually parks it on
the subscription. And modelOverride is the only way to give subagents a
different model from the main conversation, since agent() inherits the main
model when none is specified and you cannot change that from Claude Code's side.
When both quotas are close to the limit but you do not want running agents to stop, set a rate limit on a seat card in the Rack tab: requests on that seat wait in the router so at most N per minute go out. Each seat (the subscription and every provider) has its own. A request waits at most 4 minutes, and the limit lives in memory only, so a restart clears it. See docs/routing.md.
Every request takes exactly one of two lines. There are no settings for any of this.
| Subscription line | Provider line | |
|---|---|---|
| Which requests | The main conversation, anything no rule matches, rules pointing at passthrough, and anything that is not a JSON /v1/messages* request |
Requests matched by a rule that points at a provider |
| Sent to | https://api.anthropic.com + the original path and query |
{baseUrl} + the original path and query |
| Headers | Forwarded as-is, minus host, hop-by-hop headers and accept-encoding |
Rebuilt from scratch: content-type, the provider's own key, and anthropic-version, anthropic-beta, accept copied from Claude Code. Your OAuth token, cookies and x-claude-code-* headers are never sent |
| Body | The original bytes. Only a rule's modelOverride rewrites model |
model rewritten (rule modelOverride, then provider model), metadata removed, and \0 inside a tool schema's pattern swapped for the equivalent \x00 (DeepSeek cannot compile the former). Everything else is untouched: thinking, output_config, context_management, cache_control, mid-conversation system messages |
| Response | Passed through as it streams | Passed through as it streams, except one error: a context overflow in OpenAI wording is reworded to prompt is too long: <requested> tokens > <limit> maximum, numbers kept, so Claude Code compacts instead of failing |
On both lines the router never retries — Claude Code already does. If the
upstream cannot be reached it answers 502; if a stream breaks midway it drops
the connection so Claude Code resends; a body over 64 MiB gets 413. None of
the rewrites touch the cached prompt prefix, and removing metadata lets
DeepSeek reuse its cache across sessions. Every rewrite shows up per request
under Rewritten before sending in the traffic log. Details:
docs/providers.md,
docs/reliability.md.
- Both servers bind to
127.0.0.1only. - The admin API validates
OriginandHost, so a web page cannot drive it and DNS rebinding does not work. - Stored API keys are never returned to the browser — the GUI receives a masked
hint and a
__keep__sentinel. config.jsonandtraffic.logare written0600.- The traffic log records metadata only: no request bodies, no headers, no credentials.
- Provider requests are built from an empty header set — only
anthropic-version,anthropic-betaandacceptare copied over — so no client credential can be forwarded by accident. A test asserts this. - Provider requests have
metadataremoved: Claude Code puts your claude.aiaccount_uuidanddevice_idin it. The subscription line is untouched. A test asserts this. - HTTPS proxy mode's CA can only sign
api.anthropic.com(nameConstraints), its key is written0600, and it is never installed system-wide. A test proves a certificate it signs for another host, or for an IP address, is rejected.
Details and the threat model: docs/security.md.
npm test # node --test, no dependencies, no networkZero runtime and dev dependencies is a deliberate constraint — please keep it. CI runs the suite on Node 20/22/24 across Ubuntu and Windows.
The in-depth docs are written in Traditional Chinese.
| docs/configuration.md | Every config field, the fixed values, and what older config files lose |
| docs/routing.md | Rule matching, model overrides, quota switching |
| docs/observability.md | The traffic log, cache hit rates, and reading the rack |
| docs/reliability.md | Why the router hands failures straight back to Claude Code, and mid-stream disconnects |
| docs/providers.md | Provider compatibility notes, the built-in tests, and measurements |
| docs/claude-code-request-shapes.md | The request shapes Claude Code v2.1.274 actually sends and how DeepSeek handles each |
| docs/security.md | Threat model and what is and is not protected |
| docs/request-map.md | Every request Claude Code sends, how it is classified and where it goes in each mode, including the auto mode classifier |
| docs/https-proxy.md | HTTPS proxy mode for Claude Desktop: how it works, the local CA, measurements, pitfalls |
| docs/measurements.md | The measurements behind the router's design decisions |
| docs/ui-notes.md | GUI engineering rules worth keeping when editing src/ui/ |
