Clash Proxy API
Operate a running Clash / mihomo proxy from natural-language requests — "what's my
current node?", "switch to the JP node", "am I in global or rule mode?", "turn on
TUN", "reload my config", "the proxy isn't working", "there's no system-proxy
toggle on my Ubuntu box" — without re-discovering controller details or
hand-writing curl.
The reliable core is the external-controller REST API. Bundled scripts wrap
it, bounded routing observation and the OS system proxy. On a managed router,
read its project contract before choosing a command.
Invocations below are relative to this skill's directory. Run python3 scripts/clash_api.py … from here, or use the absolute path your agent resolves
for the skill. clash_api.py is stdlib-only — no uv, no pip installs.
When to use
- "What node/mode am I on?" →
status. "List my groups / nodes" → groups / proxies.
- "Switch PROXY to the Japan node" →
delay then switch.
- "Go global / rule / direct mode" →
mode.
- "Turn TUN on/off", "enable transparent proxy" →
tun.
- "Reload my config", "close all connections", "show my egress IP" →
reload / connections / egress.
- "Is the Clash API even on? / it's not working" →
doctor, then references/enable-api-by-client.md.
- "There's no system-proxy toggle" (mihomo CLI, headless, Ubuntu) →
clash_sysproxy.sh.
- The user mentions Clash, mihomo, Clash Verge (Rev), ClashX, external-controller,
9090/9097, or a proxy node by name.
When NOT to use
- Authoring subscriptions or publishing rules — use the policy repository's review/build workflow. Managed routers deploy through their own transactions.
- Mixin / Merge config — a client-side config-file feature (Clash Verge / CFW), not a runtime API. Guide the user to the client; don't script it.
- Picking/buying nodes, subscription management — out of scope.
- A one-off
curl the user already wrote — just run it.
Mental model — three layers
| Layer |
What it controls |
Tool |
| Controller API |
current node, groups, latency, mode, TUN enable, reload, connections, rules |
clash_api.py (works on any client with the API on) |
| Enable / discover |
turning the API on; finding its address+secret |
clash_api.py doctor → references/enable-api-by-client.md |
| OS / client toggles |
System Proxy, Service Mode (for TUN), Mixin |
clash_sysproxy.sh for system proxy; guidance for the rest |
Never hardcode the controller. It is not always 127.0.0.1:9090 — Clash Verge
Rev defaults to 9097, GUIs pick random ports, routers live on the LAN. Always
discover (or set CLASH_CONTROLLER).
Quick start
python3 scripts/clash_api.py doctor # discover controller + diagnose; ALWAYS start here if unsure
python3 scripts/clash_api.py status # node, mode, tun, ports, groups
doctor/status resolve the controller from, in order: --controller/--secret
flags → CLASH_CONTROLLER/CLASH_SECRET env → the optional Television hook
(~/.config/television/clash-source.sh) → local config files → probe 127.0.0.1:9090
then :9097. First reachable wins. Auth is Authorization: Bearer <secret> when a
secret is known.
Intent → command map
| User says |
Command |
| "what's my node / status" |
status (add --json for machine output) |
| "list groups / show members" |
groups --members |
| "list / search nodes" |
proxies [--filter jp] |
| "test latency of node X" |
delay "X" |
| "test all nodes in group G" |
group-delay "G" (mihomo cores only) |
| "switch group G to node X" |
switch G "X" (validates X ∈ G first) |
| "go global / rule / direct" |
mode global|rule|direct |
| "turn TUN on/off" |
tun on|off [--restart] |
| "allow LAN on/off" |
allow-lan on|off |
| "reload my config" |
reload (or reload --path /abs/config.yaml) |
| "show/close connections" |
connections / connections close --all --yes |
| "what's my egress IP" |
egress |
| "show my routing rules" |
rules [--filter netflix] |
| "is the API on / it's broken" |
doctor → guide via references/enable-api-by-client.md |
| "toggle the system proxy" |
clash_sysproxy.sh detect|on <host:port> --yes|off --yes |
Every mutating command takes --dry-run to preview without changing anything.
Workflow A — switch node safely
python3 scripts/clash_api.py status
# controller: 127.0.0.1:9097 (config: ~/.config/clash/config.yaml)
# mode: rule tun: off
# groups:
# - PROXY: now=US-01 type=Selector members=8
python3 scripts/clash_api.py delay "JP-01" --timeout-ms 3000
# JP-01: 142 ms # if this fails/timeouts, report it and do NOT switch unless asked
python3 scripts/clash_api.py switch PROXY "JP-01"
# switched PROXY -> JP-01
python3 scripts/clash_api.py status # verify now=JP-01
Test latency before switching. switch pre-validates the node is a member and, on
a bad name, prints the actual members so you can self-correct. Don't switch
URL-test/fallback groups unless asked — they reselect automatically.
Workflow B — the API isn't reachable
python3 scripts/clash_api.py doctor
# controller: UNREACHABLE ... Tried: default (127.0.0.1:9090); default (127.0.0.1:9097)
# detected: config dir: ~/.config/clash; process: mihomo
doctor reports OS, detected client, config file, and what it probed. Then open
references/enable-api-by-client.md, find the user's client, and walk them through
enabling external-controller (or copy its address/secret into CLASH_CONTROLLER
/CLASH_SECRET). Re-run doctor to confirm.
Workflow C — change mode / enable TUN
python3 scripts/clash_api.py mode global # PATCH /configs {"mode":"global"}
python3 scripts/clash_api.py tun on --restart # PATCH tun.enable, then POST /restart
TUN needs an elevated core (Service Mode / root). If it doesn't take effect,
the core isn't privileged — point the user at Service Mode in
references/enable-api-by-client.md. Preview first with --dry-run.
Workflow D — system proxy where the client has no toggle
For mihomo CLI / headless / Ubuntu (no client toggle):
bash scripts/clash_sysproxy.sh detect # current OS proxy + $http_proxy
bash scripts/clash_sysproxy.sh on 127.0.0.1:7890 --yes # macOS networksetup / GNOME gsettings
# also prints: export http_proxy=http://127.0.0.1:7890 ... (for headless shells)
bash scripts/clash_sysproxy.sh off --yes
Without --yes it previews the exact commands and changes nothing. On a platform
with no toggler it still prints export/unset lines for the shell. Use the
proxy's mixed-port (see status → ports) as host:port. TUN mode is the
alternative that needs no system proxy at all.
Available scripts
scripts/clash_api.py — Controller API client. Stdlib-only Python 3. Discovers the controller, speaks Authorization: Bearer, URL-encodes names.
- Read:
doctor, status, config, groups [--members], proxies [--filter], rules [--filter], delay <proxy>, group-delay <group>, connections, egress. Add --json to read commands for structured stdout.
- Write (all support
--dry-run): switch <group> <proxy>, mode <rule|global|direct>, tun <on|off> [--restart], allow-lan <on|off>, reload [--path P], connections close [--id X | --all --yes].
- Global:
--controller http[s]://host:port, --ca-cert PATH / CLASH_CA_CERT, --secret-file PATH / CLASH_SECRET_FILE, --read-only. Keep secrets out of command arguments; the legacy --secret flag remains compatible.
- Exit:
0 ok, 1 usage, 2 group/proxy not found (message lists real members), 3 controller unreachable, 4 op rejected (HTTP ≥300 on a write).
scripts/clash_sysproxy.sh — OS system-proxy toggle (bash 3.2). macOS networksetup / GNOME gsettings; always prints shell export/unset lines to stdout.
detect | on <host:port> [--socks H:P] | off. Flags: --yes (apply; else preview), --dry-run, --service NAME (macOS), -h.
- Exit:
0 ok/preview, 1 usage, 2 no OS toggler on this platform, 3 apply failed.
Reference files
references/managed-router-diagnosis.md — Read first for single-site diagnosis, a remote TLS controller, Nikki/ImmortalWrt, missing routing evidence, or rule candidates. Includes the report contract and links to the reusable policy knowledge base.
references/enable-api-by-client.md — Read when doctor can't reach a controller, or the user asks about System Proxy / TUN / Service Mode / Mixin. Per-client × OS matrix (mihomo CLI, Clash Verge Rev, ClashX, mihomo-party/FlClash, legacy CFW, OpenClash/Docker) for turning the API on and where the OS-level toggles live.
references/api-endpoints.md — Read when you need an operation the script doesn't wrap (streaming /traffic /logs /memory, /providers, rule-providers, /dns/query, /storage) and want raw curl. Full endpoint catalog with a "wrapped by clash_api.py" column.
Gotchas
- Managed Pi commands take precedence. Use its
just diagnose-site wrapper and verified private CA. Do not use API writes, reload, mode/TUN changes or system-proxy toggles to bypass proxy-test / proxy-enable transactions. Its policy uses TPROXY with TUN off and fail-open behavior.
- Explicit HTTPS targets never fall back to another controller. Certificate/hostname errors are failures; do not switch to HTTP or disable verification. Requests ignore environment proxy settings and reject redirects before forwarding credentials.
- A remote controller is not an egress proxy.
egress requires an explicit --proxy for remote controllers. A management API port is never a proxy data port.
observe needs traffic during its 1–60 second window. It filters exact domain and optional source IP, combines connection snapshots and bounded log lines, and can still miss short connections. Empty evidence cannot prove bypass; a matching rule cannot prove application access.
- Reports are private browsing metadata. Domain, source IP, node names, controller and timestamps may identify usage. Persist below ignored private storage with mode 0600; share a reviewed conclusion, not raw reports/configs.
- The controller is not always
127.0.0.1:9090. Clash Verge Rev defaults to 9097; GUIs pick random ports; routers are on the LAN. Discovery probes 9090+9097 and scans config files — but when it fails, read the address from the client and set CLASH_CONTROLLER. Never assume 9090.
- The proxy port is not always
7890. Derive it from live config: status prints ports (Clash Verge uses mixed-port 7897). egress and clash_sysproxy.sh should use the reported mixed/http port, not a guess.
- Enabling TUN via the API needs an elevated core.
PATCH /configs {"tun":{"enable":true}} returns 204 but TUN won't route without Service Mode / root, and usually a POST /restart (tun on --restart). If it "succeeds" but nothing changes, that's why.
- System Proxy is an OS setting, not an API concept. The controller API cannot toggle it. Use the client's toggle, or
clash_sysproxy.sh.
- Mixin / Merge is a config-file feature (Clash Verge / CFW), applied on profile activation — not a runtime call. Guide the user in the client; then
reload.
group-delay, /providers, /restart, /memory are mihomo-only. On classic Clash Premium they 404 (the script says so). status shows (mihomo/meta) when the core supports them.
- Switching a selector reroutes all traffic using that proxy — ongoing downloads, other shells'
http_proxy, browser sessions. It's reversible but live. URL-test/fallback groups may immediately reselect, so a manual switch there may not "stick".
- Never print the
secret. status reports secret: yes/no, never the value. Say a secret was used, not what it is.
$CLASH_CONFIG set-but-missing is a hard error, by design. The tool won't silently fall back to another config (which could load a different profile and leak its secret). Fix the path or unset it.
- Prefer the runtime
config.yaml over a subscription profile for controller creds — subscription profiles usually omit external-controller/secret. Discovery already scans runtime configs first.
reload --path resolves on the controller host. For a remote/container controller, the path must exist there, not on the calling machine.
1---2name: clash-proxy-api3description: Use when diagnosing an unreachable website through Clash/mihomo, observing matched rules and proxy chains, checking node/mode/latency, or controlling Clash Verge, ClashX, TUN, Mixin and the system proxy. Supports explicit HTTPS controllers, private CA verification and bounded read-only routing observations; respects managed-router transaction workflows.4---56# Clash Proxy API78Operate a running Clash / mihomo proxy from natural-language requests — "what's my9current node?", "switch to the JP node", "am I in global or rule mode?", "turn on10TUN", "reload my config", "the proxy isn't working", "there's no system-proxy11toggle on my Ubuntu box" — without re-discovering controller details or12hand-writing `curl`.1314The reliable core is the **external-controller REST API**. Bundled scripts wrap15it, bounded routing observation and the OS system proxy. On a managed router,16read its project contract before choosing a command.1718> Invocations below are relative to this skill's directory. Run `python319> scripts/clash_api.py …` from here, or use the absolute path your agent resolves20> for the skill. `clash_api.py` is **stdlib-only** — no `uv`, no pip installs.2122## When to use2324- "What node/mode am I on?" → `status`. "List my groups / nodes" → `groups` / `proxies`.25- "Switch PROXY to the Japan node" → `delay` then `switch`.26- "Go global / rule / direct mode" → `mode`.27- "Turn TUN on/off", "enable transparent proxy" → `tun`.28- "Reload my config", "close all connections", "show my egress IP" → `reload` / `connections` / `egress`.29- "Is the Clash API even on? / it's not working" → `doctor`, then `references/enable-api-by-client.md`.30- "There's no system-proxy toggle" (mihomo CLI, headless, Ubuntu) → `clash_sysproxy.sh`.31- The user mentions Clash, mihomo, Clash Verge (Rev), ClashX, external-controller, `9090`/`9097`, or a proxy node by name.3233## When NOT to use3435- **Authoring subscriptions or publishing rules** — use the policy repository's review/build workflow. Managed routers deploy through their own transactions.36- **Mixin / Merge config** — a client-side config-file feature (Clash Verge / CFW), not a runtime API. Guide the user to the client; don't script it.37- **Picking/buying nodes, subscription management** — out of scope.38- **A one-off `curl` the user already wrote** — just run it.3940## Mental model — three layers4142| Layer | What it controls | Tool |43|---|---|---|44| **Controller API** | current node, groups, latency, **mode**, **TUN enable**, reload, connections, rules | `clash_api.py` (works on any client with the API on) |45| **Enable / discover** | turning the API on; finding its address+secret | `clash_api.py doctor` → `references/enable-api-by-client.md` |46| **OS / client toggles** | **System Proxy**, Service Mode (for TUN), Mixin | `clash_sysproxy.sh` for system proxy; guidance for the rest |4748**Never hardcode the controller.** It is not always `127.0.0.1:9090` — Clash Verge49Rev defaults to `9097`, GUIs pick random ports, routers live on the LAN. Always50discover (or set `CLASH_CONTROLLER`).5152## Quick start5354```sh55python3 scripts/clash_api.py doctor # discover controller + diagnose; ALWAYS start here if unsure56python3 scripts/clash_api.py status # node, mode, tun, ports, groups57```5859`doctor`/`status` resolve the controller from, in order: `--controller`/`--secret`60flags → `CLASH_CONTROLLER`/`CLASH_SECRET` env → the optional Television hook61(`~/.config/television/clash-source.sh`) → local config files → probe `127.0.0.1:9090`62then `:9097`. First reachable wins. Auth is `Authorization: Bearer <secret>` when a63secret is known.6465## Intent → command map6667| User says | Command |68|---|---|69| "what's my node / status" | `status` (add `--json` for machine output) |70| "list groups / show members" | `groups --members` |71| "list / search nodes" | `proxies [--filter jp]` |72| "test latency of node X" | `delay "X"` |73| "test all nodes in group G" | `group-delay "G"` (mihomo cores only) |74| "switch group G to node X" | `switch G "X"` (validates X ∈ G first) |75| "go global / rule / direct" | `mode global\|rule\|direct` |76| "turn TUN on/off" | `tun on\|off [--restart]` |77| "allow LAN on/off" | `allow-lan on\|off` |78| "reload my config" | `reload` (or `reload --path /abs/config.yaml`) |79| "show/close connections" | `connections` / `connections close --all --yes` |80| "what's my egress IP" | `egress` |81| "show my routing rules" | `rules [--filter netflix]` |82| "is the API on / it's broken" | `doctor` → guide via `references/enable-api-by-client.md` |83| "toggle the system proxy" | `clash_sysproxy.sh detect\|on <host:port> --yes\|off --yes` |8485Every mutating command takes `--dry-run` to preview without changing anything.8687## Workflow A — switch node safely8889```sh90python3 scripts/clash_api.py status91# controller: 127.0.0.1:9097 (config: ~/.config/clash/config.yaml)92# mode: rule tun: off93# groups:94# - PROXY: now=US-01 type=Selector members=89596python3 scripts/clash_api.py delay "JP-01" --timeout-ms 300097# JP-01: 142 ms # if this fails/timeouts, report it and do NOT switch unless asked9899python3 scripts/clash_api.py switch PROXY "JP-01"100# switched PROXY -> JP-01101102python3 scripts/clash_api.py status # verify now=JP-01103```104105Test latency before switching. `switch` pre-validates the node is a member and, on106a bad name, prints the actual members so you can self-correct. Don't switch107URL-test/fallback groups unless asked — they reselect automatically.108109## Workflow B — the API isn't reachable110111```sh112python3 scripts/clash_api.py doctor113# controller: UNREACHABLE ... Tried: default (127.0.0.1:9090); default (127.0.0.1:9097)114# detected: config dir: ~/.config/clash; process: mihomo115```116117`doctor` reports OS, detected client, config file, and what it probed. Then open118`references/enable-api-by-client.md`, find the user's client, and walk them through119enabling `external-controller` (or copy its address/secret into `CLASH_CONTROLLER`120/`CLASH_SECRET`). Re-run `doctor` to confirm.121122## Workflow C — change mode / enable TUN123124```sh125python3 scripts/clash_api.py mode global # PATCH /configs {"mode":"global"}126python3 scripts/clash_api.py tun on --restart # PATCH tun.enable, then POST /restart127```128129TUN needs an **elevated core** (Service Mode / root). If it doesn't take effect,130the core isn't privileged — point the user at Service Mode in131`references/enable-api-by-client.md`. Preview first with `--dry-run`.132133## Workflow D — system proxy where the client has no toggle134135For mihomo CLI / headless / Ubuntu (no client toggle):136137```sh138bash scripts/clash_sysproxy.sh detect # current OS proxy + $http_proxy139bash scripts/clash_sysproxy.sh on 127.0.0.1:7890 --yes # macOS networksetup / GNOME gsettings140# also prints: export http_proxy=http://127.0.0.1:7890 ... (for headless shells)141bash scripts/clash_sysproxy.sh off --yes142```143144Without `--yes` it previews the exact commands and changes nothing. On a platform145with no toggler it still prints `export`/`unset` lines for the shell. Use the146proxy's **mixed-port** (see `status` → ports) as `host:port`. TUN mode is the147alternative that needs no system proxy at all.148149## Available scripts150151- **`scripts/clash_api.py`** — Controller API client. Stdlib-only Python 3. Discovers the controller, speaks `Authorization: Bearer`, URL-encodes names.152 - Read: `doctor`, `status`, `config`, `groups [--members]`, `proxies [--filter]`, `rules [--filter]`, `delay <proxy>`, `group-delay <group>`, `connections`, `egress`. Add `--json` to read commands for structured stdout.153 - Write (all support `--dry-run`): `switch <group> <proxy>`, `mode <rule|global|direct>`, `tun <on|off> [--restart]`, `allow-lan <on|off>`, `reload [--path P]`, `connections close [--id X | --all --yes]`.154 - Global: `--controller http[s]://host:port`, `--ca-cert PATH` / `CLASH_CA_CERT`, `--secret-file PATH` / `CLASH_SECRET_FILE`, `--read-only`. Keep secrets out of command arguments; the legacy `--secret` flag remains compatible.155 - Exit: `0` ok, `1` usage, `2` group/proxy not found (message lists real members), `3` controller unreachable, `4` op rejected (HTTP ≥300 on a write).156- **`scripts/clash_sysproxy.sh`** — OS system-proxy toggle (bash 3.2). macOS `networksetup` / GNOME `gsettings`; always prints shell `export`/`unset` lines to stdout.157 - `detect` | `on <host:port> [--socks H:P]` | `off`. Flags: `--yes` (apply; else preview), `--dry-run`, `--service NAME` (macOS), `-h`.158 - Exit: `0` ok/preview, `1` usage, `2` no OS toggler on this platform, `3` apply failed.159160## Reference files161162- `references/managed-router-diagnosis.md` — Read **first** for single-site diagnosis, a remote TLS controller, Nikki/ImmortalWrt, missing routing evidence, or rule candidates. Includes the report contract and links to the reusable policy knowledge base.163- `references/enable-api-by-client.md` — Read **when** `doctor` can't reach a controller, or the user asks about System Proxy / TUN / Service Mode / Mixin. Per-client × OS matrix (mihomo CLI, Clash Verge Rev, ClashX, mihomo-party/FlClash, legacy CFW, OpenClash/Docker) for turning the API on and where the OS-level toggles live.164- `references/api-endpoints.md` — Read **when** you need an operation the script doesn't wrap (streaming `/traffic` `/logs` `/memory`, `/providers`, rule-providers, `/dns/query`, `/storage`) and want raw `curl`. Full endpoint catalog with a "wrapped by clash_api.py" column.165166## Gotchas167168- **Managed Pi commands take precedence.** Use its `just diagnose-site` wrapper and verified private CA. Do not use API writes, reload, mode/TUN changes or system-proxy toggles to bypass `proxy-test` / `proxy-enable` transactions. Its policy uses TPROXY with TUN off and fail-open behavior.169- **Explicit HTTPS targets never fall back to another controller.** Certificate/hostname errors are failures; do not switch to HTTP or disable verification. Requests ignore environment proxy settings and reject redirects before forwarding credentials.170- **A remote controller is not an egress proxy.** `egress` requires an explicit `--proxy` for remote controllers. A management API port is never a proxy data port.171- **`observe` needs traffic during its 1–60 second window.** It filters exact domain and optional source IP, combines connection snapshots and bounded log lines, and can still miss short connections. Empty evidence cannot prove bypass; a matching rule cannot prove application access.172- **Reports are private browsing metadata.** Domain, source IP, node names, controller and timestamps may identify usage. Persist below ignored private storage with mode 0600; share a reviewed conclusion, not raw reports/configs.173- **The controller is not always `127.0.0.1:9090`.** Clash Verge Rev defaults to `9097`; GUIs pick random ports; routers are on the LAN. Discovery probes 9090+9097 and scans config files — but when it fails, read the address from the client and set `CLASH_CONTROLLER`. Never assume 9090.174- **The proxy port is not always `7890`.** Derive it from live config: `status` prints `ports` (Clash Verge uses mixed-port `7897`). `egress` and `clash_sysproxy.sh` should use the reported mixed/http port, not a guess.175- **Enabling TUN via the API needs an elevated core.** `PATCH /configs {"tun":{"enable":true}}` returns 204 but TUN won't route without Service Mode / root, and usually a `POST /restart` (`tun on --restart`). If it "succeeds" but nothing changes, that's why.176- **System Proxy is an OS setting, not an API concept.** The controller API cannot toggle it. Use the client's toggle, or `clash_sysproxy.sh`.177- **Mixin / Merge is a config-file feature** (Clash Verge / CFW), applied on profile activation — not a runtime call. Guide the user in the client; then `reload`.178- **`group-delay`, `/providers`, `/restart`, `/memory` are mihomo-only.** On classic Clash Premium they 404 (the script says so). `status` shows `(mihomo/meta)` when the core supports them.179- **Switching a selector reroutes all traffic using that proxy** — ongoing downloads, other shells' `http_proxy`, browser sessions. It's reversible but live. URL-test/fallback groups may immediately reselect, so a manual switch there may not "stick".180- **Never print the `secret`.** `status` reports `secret: yes/no`, never the value. Say a secret was used, not what it is.181- **`$CLASH_CONFIG` set-but-missing is a hard error, by design.** The tool won't silently fall back to another config (which could load a different profile and leak its secret). Fix the path or unset it.182- **Prefer the runtime `config.yaml` over a subscription profile** for controller creds — subscription profiles usually omit `external-controller`/`secret`. Discovery already scans runtime configs first.183- **`reload --path` resolves on the controller host.** For a remote/container controller, the path must exist *there*, not on the calling machine.