Clash API Node Switching: Advanced Automation Guide

What Clash API Automation Ought to Actually Do Demystified

The examples use Shell and Python because they are available on the majority of 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.

The majority of Clash users begin with a by hand 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 delay changes sharply during the day.

One more angle: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 delay, and move a proxy group to a working node.

The important distinction is that the script does not have 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 somewhat than a theoretical monitoring platform. You will learn how to expose the controller safely, discover proxy names, test delay with the correct URL, refresh a selector group, and add safeguards so a temporary timeout does not cause constant switching.

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

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

Prepare and Secure the External Controller at a Glance

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 ought to normally use permissions such as chmod 600.

A detail worth adding:The controller is an HTTP UI 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 typically 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 UI 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.

Said differently:For a local automation script, binding to 127.0.0.1 is normally sufficient. If automation executes on another machine, place the controller behind a private network, firewall rule, or authenticated tunnel instead of publishing it straight away to the internet.

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

Test the controller before writing automation at a Glance

Switching a group that is not referenced by whichever 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.

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"

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

In practice that means:A successful response is a JSON object containing proxy definitions and, in the majority of versions, a special GLOBAL or DIRECT entry. If you receive 401 Unauthorized, check the secret and header format. A connection refusal typically 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 failure: 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.

Choose a Useful Node Health Test Broken Down

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

Worth noting:A delay test is not the same as a full guarantee that a node will work for every destination. The controller typically 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.

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"

Choose a steady HTTPS endpoint that returns quickly and does not require login. Numerous Clash-compatible cores accept a request shaped like this:

One more angle: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 failure when the request times out.

A number 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 cut downs false negatives but makes failover slower. For a personal workstation, five seconds is a reasonable starting point.

A detail worth adding:You are able 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.

Delay alone ought 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.

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.

Launch 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 From the Ground Up

The the majority of dependable 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 refreshs 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 refresh.
  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 whichever node that your local naming policy marks as unsuitable.
  4. Measure candidates: call the delay endpoint with a bounded timeout and store both successful delay 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. Refresh 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 in Short

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.

Said differently: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.

#!/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)"

Adjust the group name and test URL, and install jq if your system does not already provide it.

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

Python automation with hysteresis Decoded

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

In practice that means:Python is useful when you want clearer failure handling, persistent state, or a more sophisticated policy. The following example uses the standard library only.

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")

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.

The use of hysteresis is important. Without it, two nodes with nearly identical delay 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.

Guardrails for Long-Running Automation Demystified

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

Worth noting:A node switch can interrupt active downloads, WebSocket sessions, remote shells, and API requests. For that reason, the automation ought 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 delay 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 steady group for work services, one elastic group for ordinary browsing, and one by hand controlled group for debugging.

Subscription refreshs can rename or remove nodes. A script ought 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 delay, and reason for every decision.

  • 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: delay to the test URL ought 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.

One more angle:Avoid logging full subscription URLs because they may contain credentials. When a script fails, the log ought to tell you whether the failure was auth, JSON parsing, timeout, missing node, or an API compatibility issue.

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

Troubleshoot Failed API Switching at a Glance

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

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 detail worth adding:A failure may also mean that the selected object is a group somewhat 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 failure, verify that the target is a selectable group. A number 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.

Said differently: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.

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 refresh, and only afterward add scheduling.

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

Frequently Asked Questions Broken Down

Ought to I expose the Clash controller to my LAN? in Practice

Typically not. A local automation script ought 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 delay node always provide the best connection? at a Glance

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 delay as a screening signal, then verify the actual workload and avoid switching for tiny differences.

Why does automatic switching keep oscillating between two nodes? in Short

Oscillation normally comes from a policy with no hysteresis. Add a minimum improvement threshold, consecutive-failure requirements, and a cooldown period.

In practice that means: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? Decoded

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 handy choices, while mobile clients may expose fewer background-automation settings 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 plain audit trail; those alternatives may still be useful, but they often hide selection state, lack dependable controller documentation, or become difficult to maintain after a subscription refresh. 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 are able to download a suitable Clash client and build the failover process step by step without giving up manual control.