| name | clash-proxy-api |
| description | Discover and drive a running Clash/mihomo proxy through its external-controller API — show the current node/mode/latency, switch proxy groups, set mode (rule/global/direct), toggle TUN, reload config, close connections, and toggle the OS system proxy. Use when the user asks to check or change their Clash/mihomo/Clash Verge (Rev)/ClashX proxy, switch nodes, test node latency, enable TUN, set up Service Mode or Mixin, toggle the system proxy on macOS/Linux, or fix an unreachable Clash API. |
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. Two bundled scripts wrap
it and the OS system-proxy; two references cover enabling the API per client and
the raw endpoint surface.
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
- Editing subscription/YAML by hand or writing rules — the API reloads and toggles; it doesn't author config. Edit the file, then
reload.
- 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
python3 scripts/clash_api.py status
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
python3 scripts/clash_api.py delay "JP-01" --timeout-ms 3000
python3 scripts/clash_api.py switch PROXY "JP-01"
python3 scripts/clash_api.py status
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
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
python3 scripts/clash_api.py tun on --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
bash scripts/clash_sysproxy.sh on 127.0.0.1:7890 --yes
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 host:port, --secret S (overrides env/discovered secret).
- 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/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
- 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.