9Router is a local routing layer, not a tunnel. It exposes an OpenAI-compatible endpoint on loopback (the project defaults to http://127.0.0.1:20128/v1; check your build), lets Claude Code, Codex, Cursor, and Cline all point at it, and decides which upstream each request goes to—with fallback when one is rate limited. When it fails in a restricted network, the routing is rarely the problem; egress is.
Before touching anything, read how the individual tools connect: How to use Claude Code, How to use Codex CLI, Tun for AI tools, and terminal proxy variables. This page is only about the one extra hop in the middle.
What 9Router actually does
Think of it as a thin proxy in front of your model providers. Each tool points at one Base URL; 9Router then maps the request to a concrete upstream—Anthropic, OpenAI, a self-hosted endpoint—and rewrites the format if needed. That single indirection buys you three things a raw proxy setup cannot:
- One config surface. Switch providers, add a cheap key, or change the format in one place instead of editing every tool's environment.
- Automatic fallback. When an upstream returns 401 or hits quota, 9Router can route the next request to the next credential without the CLI noticing.
- Unified usage. Token counts and request logs land in one dashboard rather than scattered across four terminal windows.
None of that changes how packets leave your machine. 9Router still has to reach the open internet, and in a restricted network that means it has to go through Clash Verge. The two halves fail in completely different ways, which is why the next section separates them.
Separate the two layers
| Layer | Owns | Failure signature |
|---|---|---|
| Routing (9Router) | Upstream choice, format conversion, fallback, usage | One upstream 401s or runs out of quota; others still work |
| Egress (Clash Verge) | Getting packets out | Every upstream times out while the local endpoint answers instantly |
Quick test: if the dashboard loads and model lists populate but every chat request hangs, stop swapping models and check the proxy. If even the dashboard cannot reach the upstream to list models, the routing layer is suspect and you should check credentials and Base URL first.
Attach the proxy to the router, not to each CLI
- Export
HTTPS_PROXY/HTTP_PROXY(your Clash Verge mixed port—read it off the UI) in the shell that starts 9Router. The project accepts upper and lower case, plusALL_PROXYandNO_PROXY. -
Put loopback in
NO_PROXY:127.0.0.1,localhost. Otherwise CLI calls to your own endpoint get shoved at the proxy and the router looks dead while it is running fine. - Do not set proxy variables in the shells that run your CLIs. They only need the Base URL and a local key. Setting the proxy on the CLI too creates a double-hop—the CLI proxies to 9Router, which proxies again to Clash Verge—and doubles the number of places a timeout can strike.
- Prefer no variables at all? Use Tun—process-level capture, loopback normally untouched. Just confirm your rules do not route private addresses into the tunnel (direct rules).
Write 127.0.0.1 rather than localhost. The README calls this out: on some systems localhost resolves to IPv6 first, and an IPv4-only listener then looks like a dead service. The same trap also bites the CLIs: always give them http://127.0.0.1:20128/v1, never http://localhost:20128/v1, unless you have confirmed your stack listens on both families.
Environment variable reference
The router reads the standard proxy variables. The full set, in the order most configs use them:
| Variable | Meaning | Typical value |
|---|---|---|
| HTTPS_PROXY / https_proxy | Proxy for outbound TLS requests | http://127.0.0.1:7897 |
| HTTP_PROXY / http_proxy | Proxy for plaintext outbound | http://127.0.0.1:7897 |
| ALL_PROXY / all_proxy | Fallthrough used when the above are absent | http://127.0.0.1:7897 |
| NO_PROXY / no_proxy | Comma list of hosts reached directly | 127.0.0.1,localhost |
Two common mistakes: a trailing slash on the proxy URL (some runtimes reject http://127.0.0.1:7897/) and a missing NO_PROXY that routes the dashboard's own calls back through Clash Verge and then into a loop. Keep the URL bare and the loopback excluded.
Acceptance: one layer at a time
- Clash Verge connected to a working node; a foreign site opens in the browser (latency test).
- Start 9Router with proxy variables and confirm from its own logs that upstreams and model lists resolve.
- Send a minimal request from one CLI—have it read a single file.
- Only then add the second and third tool. Wiring everything at once turns the request log into noise.
A clean minimal check that skips the CLIs entirely: hit the endpoint with curl and confirm the proxy is actually used. If this returns model data, the egress path is proven before any agent is involved:
curl -x http://127.0.0.1:7897 \
http://127.0.0.1:20128/v1/models \
-H "Authorization: Bearer $LOCAL_KEY"
Failure table
| Symptom | Likely cause | Action |
|---|---|---|
| All upstreams time out | Router process has no proxy | Set vars in its shell, restart the service |
| CLI cannot reach local endpoint | Loopback proxied / wrote localhost | Add NO_PROXY, switch to 127.0.0.1 |
| Port will not bind | Port in use | Change port, update every Base URL—see port conflicts |
| One upstream 401 / quota | Credential or plan | Fix in the dashboard; not a network issue |
| Docker cannot reach host proxy | 127.0.0.1 means the container | Use the host address or host networking |
Docker: the 127.0.0.1 trap
Running 9Router in a container changes what 127.0.0.1 means. Inside the container, 127.0.0.1:7897 is the container itself, not your host's Clash Verge. Two fixes, in order of preference:
- Host networking. Run with
--network hoston Linux; the container then shares the host's loopback and127.0.0.1:7897resolves to Clash Verge. Simplest, but loses network isolation. - Host gateway address. Point the proxy at the host's Docker bridge address (often
http://172.17.0.1:7897on Linux, orhost.docker.internalon macOS/Windows) and keepNO_PROXYcovering the loopback the CLIs use.
The CLIs, if also containerized, need the same treatment—or simply run them on the host and only containerize the router.
Is it worth installing?
With a single subscription and rare model switching, two shell aliases beat another long-running service. It earns its keep when you juggle several subscriptions or cheap keys, hit rate limits often enough to want automatic fallback, or run multiple CLIs and want usage in one place.
| Your situation | Verdict | Reason |
|---|---|---|
| One key, one tool | Skip it | A Base URL plus an env var is all you need |
| Two subscriptions, manual switching | Maybe | Fallback saves a mid-session key swap |
| Several keys + rate limits | Install | Automatic fallback pays for itself on day one |
| Many CLIs, one dashboard | Install | Central usage beats four open logs |
The UI usually also ships a group of man-in-the-middle helpers that intercept IDE traffic. Those alter your machine's trust chain and often conflict with vendor terms—skip them unless you know exactly why you need them.
Security
The dashboard stores OAuth grants and multiple API keys. Keep it off the public internet, change default credentials, require a Bearer key on the API, and back up the data directory. When handing keys to a teammate, use an encrypted channel rather than a group chat—see how teams share API keys. Treat the loopback endpoint as a secret too: any process on the machine can call it, so on a shared host bind it to a non-default port and rely on local firewall rules.
Wrap-up
9Router decides where a request goes; Clash Verge decides whether it can get there. Proxy on the router, loopback bypassed, addresses written as 127.0.0.1—then add tools one by one. Client: download center. Single-tool debugging: Claude Code proxy, Codex CLI proxy.