App Proxy

9Router Behind a Proxy: One Local Endpoint for Claude Code, Codex, and Friends

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

  1. 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, plus ALL_PROXY and NO_PROXY.
  2. 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.
  3. 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.
  4. 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

  1. Clash Verge connected to a working node; a foreign site opens in the browser (latency test).
  2. Start 9Router with proxy variables and confirm from its own logs that upstreams and model lists resolve.
  3. Send a minimal request from one CLI—have it read a single file.
  4. 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 host on Linux; the container then shares the host's loopback and 127.0.0.1:7897 resolves 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:7897 on Linux, or host.docker.internal on macOS/Windows) and keep NO_PROXY covering 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.

A local router still needs a working exit

Upstream calls still hit Anthropic, OpenAI, and friends. Install Clash Verge Rev from the download center, attach the proxy to the router process, then run the acceptance steps below.