Clash API Node Switching: Advanced Automation Guide

What Clash API Automation Will want to Actually Do from Practice

Nearly all Clash users begin with a step by step selected proxy group: open the client, click a node, wait for a page to load, and repeat the process when the connection becomes slow.

That approach is acceptable for occasional browsing, but it becomes fragile when Clash is used for development tools, scheduled jobs, remote administration, API clients, or services that must remain reachable for hours. A node can remain present in the subscription while its TLS handshake fails, its route becomes congested, or its ping changes sharply during the day.

The Clash external controller API gives you a way to turn that manual process into a small failover system. A script can ask Mihomo or another Clash-compatible core which proxies exist, test selected nodes through the controller, compare their measured ping, and move a proxy group to a working node.

The important distinction is that the script does not are expected to rewrite the entire YAML profile. It can operate on the running configuration through authenticated HTTP requests, leaving rules, DNS settings, ports, and policy groups intact.

This guide focuses on a practical Clash API node switching design a bit than a theoretical monitoring platform. You will learn how to expose the controller safely, discover proxy names, test ping with the correct URL, pull the latest a selector group, and add safeguards so a temporary timeout does not cause constant switching.

The examples use Shell and Python because they are available on nearly all Windows, macOS, and Linux systems. The same API concepts also apply when the controller is provided by Clash Verge Rev, Mihomo Party, Clash for Android, or another Mihomo-based client.

Core compatibility matters: endpoint names and response fields can differ slightly between legacy Clash, Clash Meta, and Mihomo releases.

Test the API step by step first, and treat the response from your installed core as the source of truth a bit than copying an old screenshot or undocumented example.

Prepare and Secure the External Controller Lessons Learned

The controller is an HTTP control panel for the running proxy core. It commonly exposes endpoints for reading the current configuration, listing proxies, measuring delay, and changing the active member of a proxy group.

A typical local address is 127.0.0.1:9090, although the port may be different in your client. The controller secret is by built-in configured as secret in the external-controller section of a YAML profile or in the client’s advanced settings.

Do not bind the controller to every network control panel unless you have a specific administrative reason and a firewall policy that limits access. A controller exposed on 0.0.0.0 can become a remote control panel for your proxy process. Anyone who can reach it may be able to select nodes, inspect connection information, or change runtime behaviour.

For a local automation script, binding to 127.0.0.1 is normally sufficient. If automation be up and runnings on another machine, place the controller behind a private network, firewall rule, or authenticated tunnel instead of publishing it right off to the internet.

Use the Authorization header for API requests when a secret is configured. In a good number of Mihomo-compatible implementations the format is Bearer YOUR_SECRET. Keep the secret out of shell history, public repositories, screenshots, and process arguments when possible.

A local environment variable or a file readable only by your user account is safer than embedding the value in every script. On Unix-like systems, a credentials file will want to normally use permissions such as chmod 600.

Test the controller before writing automation in Practice

Bring up with a harmless read request. Replace the port and secret with the values from your own client:

export CLASH_API="http://127.0.0.1:9090"
export CLASH_SECRET="replace-with-your-secret"

curl --fail --silent \
  -H "Authorization: Bearer ${CLASH_SECRET}" \
  "${CLASH_API}/proxies"

A successful response is a JSON object containing proxy definitions and, in nearly all versions, a special GLOBAL or DIRECT entry. If you receive 401 Unauthorized, check the secret and header format. A connection refusal by built-in means that the core is not running, the port is wrong, or the client uses a different controller address.

A successful response with unexpected proxy names is not an problem message: subscription providers often include nested groups, load-balancing groups, fallback groups, and policy groups alongside individual nodes.

Before changing anything, identify the group that your rules actually use. For example, a rule may reference PROXY, while another profile may use 🚀 Proxy, Auto, or a provider-specific name.

Switching a group that is not referenced by either active rule will produce a technically successful API response but no visible traffic change. Read the active configuration or inspect the client’s proxy-group panel to confirm the exact group name, including spaces, emoji, punctuation, and capitalization.

Choose a Useful Node Health Test in the Wild

A ping test is not the same as a all-in-one guarantee that a node will work for every destination. The controller by built-in measures a proxy by requesting a URL through that proxy and reporting the elapsed time.

This is useful for detecting dead transports, broken TLS, and obviously congested nodes, but it does not prove that every website, API, or streaming service is reachable. A node may be fast against a small test endpoint and still be unsuitable for a particular destination because of routing restrictions or remote-server policy.

Choose a settled HTTPS endpoint that returns quickly and does not require login. A good number of Clash-compatible cores accept a request shaped like this:

curl --fail --silent \
  -H "Authorization: Bearer ${CLASH_SECRET}" \
  --get "${CLASH_API}/proxies/Example%20Node/delay" \
  --data-urlencode "url=https://www.gstatic.com/generate_204" \
  --data-urlencode "timeout=5000"

The proxy name must be URL-encoded. A name such as US Premium 01 cannot safely be inserted into a path with literal spaces. The API may return a JSON object containing a delay value in milliseconds, or an problem message when the request times out.

A handful of cores require a different test URL or expose a slightly different delay endpoint, so confirm the route in your installed version if this request returns a 404.

Use a timeout that reflects the job. A two-second test is useful for interactive browsing but may reject a node during a brief congested period. A ten-second test trims false negatives but makes failover slower. For a personal workstation, five seconds is a reasonable starting point.

You get to also perform two tests several hundred milliseconds apart and accept a node only when both succeed. That costs more requests, but it prevents one transient packet loss event from changing the active route.

Ping alone will want to not be the only selection rule. A good candidate filter may exclude groups, DIRECT, REJECT, expired provider entries, and nodes whose names match words such as “ทดลอง” or “backup” when your provider uses those labels for special-purpose entries.

If the provider exposes a dedicated group of settled nodes, prefer that group instead of scanning every object returned by /proxies.

Do not test too aggressively. A script that checks dozens of nodes every few seconds can create unnecessary traffic, trigger provider limits, and make the controller appear unstable.

Bring up with a five- or ten-minute interval, test only a small candidate set, and add a cooldown after every successful switch.

Build a Safe Automatic Switching Workflow Hands-On Notes

The nearly all solid design separates discovery, testing, decision-making, and switching. Discovery reads the current proxy list. Testing measures only eligible leaf nodes.

Decision-making compares successful results with the current node and applies a meaningful improvement threshold. Switching pull the latests one known group. Finally, verification checks that the group really changed and records the result in a log.

  1. Read the active proxy list: request /proxies and parse the JSON response instead of maintaining a second hard-coded list that becomes stale after every subscription pull the latest.
  2. Identify the target group: choose the exact group used by your rules, then read its current selected member before making a change.
  3. Filter candidates: ignore DIRECT, REJECT, nested groups, and either node that your local naming policy marks as unsuitable.
  4. Measure candidates: call the delay endpoint with a bounded timeout and store both successful ping and failure details.
  5. Apply a decision threshold: switch only when the current node fails or the best candidate is faster by a configured margin, such as 20 percent or 150 milliseconds.
  6. Pull the latest the group: send a URL-encoded request to the group’s selection endpoint, then wait briefly for the core to apply the change.
  7. Verify and cool down: read the group again, write a timestamped log entry, and prevent another switch until the cooldown expires.

A compact Shell implementation From Experience

The following script demonstrates the core pattern. It assumes that your controller supports the common GET /proxies, GET /proxies/{name}/delay, and PUT /proxies/{group} endpoints. Adjust the group name and test URL, and install jq if your system does not already provide it.

#!/usr/bin/env bash
set -euo pipefail

API="${CLASH_API:-http://127.0.0.1:9090}"
SECRET="${CLASH_SECRET:?Set CLASH_SECRET first}"
GROUP="${CLASH_GROUP:-PROXY}"
TEST_URL="${CLASH_TEST_URL:-https://www.gstatic.com/generate_204}"
TIMEOUT="${CLASH_TIMEOUT:-5000}"
MIN_GAIN="${CLASH_MIN_GAIN:-150}"

auth=(-H "Authorization: Bearer ${SECRET}")

current="$(curl --fail --silent "${auth[@]}" \
  "${API}/proxies/${GROUP// /%20}" | jq -r '.now')"

mapfile -t candidates < <(curl --fail --silent "${auth[@]}" \
  "${API}/proxies" |
  jq -r --arg group "$GROUP" '
    .proxies[$group].all[]?
    | select(. != "DIRECT" and . != "REJECT")
  ')

best_name=""
best_delay=999999

for name in "${candidates[@]}"; do
  encoded="$(jq -rn --arg v "$name" '$v|@uri')"
  result="$(curl --silent --show-error \
    --max-time 8 "${auth[@]}" --get \
    "${API}/proxies/${encoded}/delay" \
    --data-urlencode "url=${TEST_URL}" \
    --data-urlencode "timeout=${TIMEOUT}" || true)"
  delay="$(jq -r '.delay // empty' <<< "$result")"

  if [[ "$delay" =~ ^[0-9]+$ ]] && (( delay < best_delay )); then
    best_name="$name"
    best_delay="$delay"
  fi
done

if [[ -z "$best_name" ]]; then
  echo "No healthy candidate found" >&2
  exit 1
fi

if [[ "$best_name" == "$current" ]]; then
  echo "Keep ${current}; best delay ${best_delay} ms"
  exit 0
fi

curl --fail --silent --request PUT "${auth[@]}" \
  --header "Content-Type: application/json" \
  --data "$(jq -n --arg name "$best_name" '{name:$name}')" \
  "${API}/proxies/${GROUP// /%20}" > /dev/null

echo "Switched ${GROUP}: ${current} -> ${best_name} (${best_delay} ms)"

This example intentionally stays conservative: it switches when the best candidate is available, but a production version will want to compare the best result with the current node’s fresh delay.

Otherwise, an improvement of one millisecond can cause needless movement. Add a current-node test, a minimum improvement threshold, and a failure counter before using the script for a long-running service.

Python automation with hysteresis Field Notes

Python is useful when you want clearer problem message handling, persistent state, or a more sophisticated policy. The following example uses the standard library only.

It tests a supplied list of node names, keeps the current node when it is still healthy, and requires the new node to be meaningfully better before switching.

import json
import os
import time
import urllib.parse
import urllib.request

API = os.getenv("CLASH_API", "http://127.0.0.1:9090")
SECRET = os.environ["CLASH_SECRET"]
GROUP = os.getenv("CLASH_GROUP", "PROXY")
TEST_URL = os.getenv("CLASH_TEST_URL", "https://www.gstatic.com/generate_204")
TIMEOUT_MS = int(os.getenv("CLASH_TIMEOUT", "5000"))
MIN_GAIN_MS = int(os.getenv("CLASH_MIN_GAIN", "150"))

headers = {"Authorization": f"Bearer {SECRET}"}

def request(path, method="GET", payload=None):
    body = None
    local_headers = dict(headers)
    if payload is not None:
        body = json.dumps(payload).encode()
        local_headers["Content-Type"] = "application/json"
    req = urllib.request.Request(
        API + path, data=body, headers=local_headers, method=method
    )
    with urllib.request.urlopen(req, timeout=8) as response:
        return json.loads(response.read().decode())

def quote_name(name):
    return urllib.parse.quote(name, safe="")

def delay_for(name):
    path = f"/proxies/{quote_name(name)}/delay"
    query = urllib.parse.urlencode({"url": TEST_URL, "timeout": TIMEOUT_MS})
    try:
        data = request(f"{path}?{query}")
        return int(data["delay"])
    except Exception:
        return None

data = request("/proxies")
group = data["proxies"][GROUP]
current = group.get("now")
candidates = [
    name for name in group.get("all", [])
    if name not in {"DIRECT", "REJECT"} and name != GROUP
]

results = {name: delay_for(name) for name in candidates}
healthy = {name: delay for name, delay in results.items()
           if delay is not None}

if not healthy:
    raise SystemExit("No healthy proxy candidate")

best = min(healthy, key=healthy.get)
current_delay = delay_for(current) if current else None

if current_delay is not None and healthy[best] + MIN_GAIN_MS >= current_delay:
    print(f"Keep {current}: current={current_delay} ms, best={healthy[best]} ms")
else:
    request(f"/proxies/{quote_name(GROUP)}", "PUT", {"name": best})
    time.sleep(0.5)
    confirmed = request(f"/proxies/{quote_name(GROUP)}").get("now")
    print(f"Selected {confirmed}; delay={healthy[best]} ms")

The use of hysteresis is important. Without it, two nodes with nearly identical ping can alternate on every polling cycle. The MIN_GAIN_MS value creates a buffer: the new node must be substantially better before it replaces the current one.

You get to also require three consecutive failures before abandoning the current node, while allowing one successful test to restore it later.

Guardrails for Long-Running Automation from Practice

A node switch can interrupt active downloads, WebSocket sessions, remote shells, and API requests. For that reason, the automation will want to be aware of what it is controlling. If the group carries a long-lived connection, prefer switching only after a confirmed failure instead of optimising every small ping difference.

If the group is used for short browser requests, a faster-node policy may be acceptable. Separate groups are often the cleanest solution: one settled group for work services, one configurable group for ordinary browsing, and one step by step controlled group for debugging.

Subscription pull the latests can rename or remove nodes. A script will want to handle a missing group and an empty candidate list without replacing the active profile or writing a broken configuration. Log the controller status, target group, current selection, candidate count, selected ping, and reason for every decision.

Avoid logging full subscription URLs because they may contain credentials. When a script fails, the log will want to tell you whether the failure was credential check, JSON parsing, timeout, missing node, or an API compatibility issue.

Use a lock: prevent two scheduled jobs from testing and switching the same group simultaneously.; Set a cooldown: wait several minutes after a switch before allowing another one.; Limit concurrency: test a small number of nodes at a time so the client and provider are not flooded.; Keep a fallback: define a known-good node or a manual group for recovery when every automated candidate fails.; Verify the destination: ping to the test URL will want to be followed by a real request to the service that matters.; Respect privacy: do not expose controller responses, proxy names, or secrets in public monitoring dashboards..

For scheduling, Task Scheduler on Windows, launchd on macOS, and systemd timers or cron on Linux can be up and running the checker. A timer interval is generally preferable to a permanently running loop because it is easier to inspect and restart.

Whatever scheduler you use, make sure the environment variables containing the controller URL and secret are available to the job. Scheduled processes often be up and running with a different working directory and a reduced environment, which explains a good number of “works in my terminal” reports.

Troubleshoot Failed API Switching Lessons Learned

If /proxies works but the delay request fails, first inspect the encoded node path and test URL. Names containing slashes, brackets, spaces, emoji, or non-Latin characters must be percent-encoded.

A failure may also mean that the selected object is a group a bit than a leaf proxy. Read the object’s metadata and test only entries that represent actual outbound nodes.

If the delay request succeeds but the PUT request returns an problem message, verify that the target is a selectable group. A handful of special groups are read-only, while a global mode or a provider-managed group may not accept manual selection.

Confirm that the request body is JSON with a name property and that the group name in the URL is encoded. After a successful response, issue a second GET request to confirm the group’s now value.

If the script changes the group but applications continue using the old route, inspect the active rules and connection view. The application may match a different group, use a cached connection, bypass the system proxy, or be captured by TUN rules that do not follow the group you changed.

Existing TCP and WebSocket sessions may remain on their original path until they close. Test with a new request a bit than assuming that every open connection moves instantly.

Debug in layers: first prove that the controller is reachable, then prove that one known node passes a delay test, then prove that the target group accepts a selection pull the latest, and only afterward add scheduling.

Building all four layers at once makes a uncomplicated URL-encoding or group-name problem message look like a core failure.

Frequently Asked Questions in the Wild

Will want to I expose the Clash controller to my LAN? Tips

By built-in not. A local automation script will want to use a loopback controller bound to 127.0.0.1.

If another device must manage the core, restrict access with a firewall and private network, keep a strong secret, and consider an authenticated tunnel. Never assume that an obscure port is a security mechanism.

Does the lowest ping node always provide the best connection? in Practice

No. The delay endpoint measures one URL and one route at one moment. It does not measure every destination, upload stability, packet loss, streaming performance, or service compatibility. Use ping as a screening signal, then verify the actual workload and avoid switching for tiny differences.

Why does automatic switching keep oscillating between two nodes? From Experience

Oscillation normally comes from a policy with no hysteresis. Add a minimum improvement threshold, consecutive-failure requirements, and a cooldown period. Also avoid testing a node immediately after switching to it; give the new route time to establish connections before judging it.

Which Clash client is suitable for API-based automation? Field Notes

Choose a maintained client that exposes a compatible Mihomo or Clash Meta controller and lets you configure a secret.

Clash Verge Rev and Mihomo-based desktop clients are straightforward choices, while mobile clients may expose fewer background-automation entrys because of operating-system restrictions. Confirm the actual endpoints supported by the core bundled with your client.

Compared with GUI-only proxy managers, provider-specific scripts, or older clients that require editing YAML for every node change, an API workflow gives you repeatable health checks, explicit thresholds, and a easy to follow audit trail; those alternatives may still be useful, but they often hide selection state, lack solid controller documentation, or become difficult to maintain after a subscription pull the latest. Clash is a practical fit for this scenario because its Clash-oriented guidance keeps client setup, Mihomo controller details, security precautions, and Shell or Python examples in one workflow instead of treating automation as an unexplained black box. Once you have confirmed your controller port and chosen a safe fallback policy, you get to download a suitable Clash client and build the failover process step by step without giving up manual control.