Clash External Controller API: Advanced Node Switching Guide

What the Clash External Controller Actually Controls Explained

The Clash external-controller API is a local HTTP interface exposed by Clash, Clash Verge Rev, Mihomo, and other clients built around a compatible core. It lets another program inspect runtime state and perform selected control actions without opening the graphical client. Instead of manually clicking through the proxy page, you can query the active configuration, list available nodes, test latency, select a member inside a proxy group, inspect live connections, and build an automatic node-switching workflow around those operations.

This is different from changing a YAML profile on disk. The API operates on the running core and usually changes the current runtime selection immediately. If you select a different member in a group called PROXY, existing connections may continue until they close, while new connections use the newly selected node. A profile reload, client restart, or group definition change can replace that runtime choice, so production scripts should distinguish between a temporary switch and a persistent configuration change.

Typical controller addresses are 127.0.0.1:9090 or 127.0.0.1:9097, but the port depends on your client and profile. Mihomo commonly uses a configuration value similar to external-controller: 127.0.0.1:9090. Some desktop clients expose the same setting in an advanced settings panel, while others place it inside the generated YAML. Do not assume that the port shown for the mixed HTTP and SOCKS listener is also the controller port. They are separate services with different purposes.

Important distinction: the mixed port carries proxied application traffic; the external controller receives administrative API requests. A browser configured to use 127.0.0.1:7890 does not prove that the controller is available at 127.0.0.1:7890.

The controller is powerful because it exposes live state rather than only static settings. A node can look attractive in a provider list but fail at the moment a request is made. By combining the group endpoint, the proxy endpoint, and latency tests, a script can make a decision based on current reachability. This makes the API useful for developers running long downloads, CI jobs, terminal tools, media workflows, or services that need to recover from a bad node without requiring a person to watch the Clash dashboard.

Enable and Secure the Controller Before Automating It in Detail

Start by confirming that the running client actually supports an external controller. Open the client settings or inspect the active profile, then locate External Controller, API controller, or the equivalent Mihomo option. Prefer a loopback binding such as 127.0.0.1:9090 when the automation runs on the same computer. Avoid binding to 0.0.0.0 unless you have a clear remote-management requirement and a firewall policy that limits access to a trusted network.

Authentication is normally supplied through the secret configuration field. A request then carries the value in the Authorization header using the Bearer format. For example, a protected local controller can be queried like this:

curl -s \
  -H "Authorization: Bearer YOUR_CONTROLLER_SECRET" \
  http://127.0.0.1:9090/version

If the controller has no secret, an accidental local process may be able to change your selected node or read connection metadata. On a personal machine, “local only” is safer than an internet-facing listener, but it is not a substitute for authentication. Browser extensions, downloaded scripts, malware, containers, and other users on a shared system may still reach loopback services. Set a secret even when you believe the API is not exposed beyond the host.

  1. Choose a loopback address: bind the controller to 127.0.0.1 or the IPv6 loopback equivalent instead of a public interface.
  2. Create a long secret: use a randomly generated value rather than reusing your subscription URL, password, or mixed-port credentials.
  3. Restart or reload the core: some clients only apply controller changes after a core restart, while others reload the active configuration.
  4. Test authentication: request /version and confirm that an invalid or missing token returns an authorization error.
  5. Restrict the automation file: keep the token in an environment variable or a protected secret file, not directly in a script committed to Git.

On Windows, an environment variable can be created in PowerShell and consumed by a script. On macOS and Linux, the same pattern works in a shell profile or a service manager. Do not place the controller token in a URL query string because shell history, reverse proxies, and access logs may retain it. Also avoid printing the complete request headers when debugging CI logs.

$env:CLASH_API_SECRET = "replace-with-a-random-secret"
curl.exe -H "Authorization: Bearer $env:CLASH_API_SECRET" `
  http://127.0.0.1:9090/proxies

Before continuing, verify that the API port is reachable and that the JSON response is from the expected Clash instance. Multiple clients can run on one machine, especially after testing Clash for Windows, Clash Verge Rev, or Mihomo Party. A successful response from the wrong port can lead you to switch nodes in one client while your applications use another. Record the controller port, mixed port, active profile, and core version together in your operational notes.

Inspect Proxy Groups and Test Nodes Safely Made Clear

The most useful discovery endpoint is usually /proxies. It returns proxy definitions and groups in a JSON object. A group commonly includes fields such as type, all, and now. The all array contains selectable members, while now identifies the current member. The exact response shape can vary between legacy Clash cores and newer Mihomo releases, so scripts should check for missing fields rather than assuming every proxy is a selectable node.

curl -s \
  -H "Authorization: Bearer $CLASH_API_SECRET" \
  http://127.0.0.1:9090/proxies | jq '.proxies.PROXY'

Do not confuse a proxy group with a leaf proxy. A group named PROXY, Auto, or 🚀 Proxy may contain nodes, nested groups, or the special DIRECT and REJECT entries. The group name is part of the provider configuration and may contain spaces, emoji, punctuation, or non-English text. When building a URL, encode the name correctly instead of manually concatenating unsafe characters.

Latency testing is performed through an endpoint similar to /proxies/{name}/delay. The request generally needs a test URL and a timeout value. A successful HTTP response means the controller accepted the test, not necessarily that the node is suitable for your application. A lightweight web resource may measure only DNS, TLS, and a small response; it does not prove that a long WebSocket, large upload, streaming API, or region-restricted service will work.

curl -G -s \
  -H "Authorization: Bearer $CLASH_API_SECRET" \
  --data-urlencode "url=https://www.gstatic.com/generate_204" \
  --data-urlencode "timeout=5000" \
  "http://127.0.0.1:9090/proxies/Node%20A/delay"

A practical health check should combine several signals. First, verify that the node exists in the current response. Second, measure latency with a timeout that reflects the use case. Third, reject HTTP errors, timeouts, and implausibly unstable results. Fourth, apply a policy that prevents a node with slightly better latency from replacing a stable node every few seconds. This last point matters because automatic switching can create more disruption than a slow but reliable connection.

Signal What it tells you What it does not prove
Delay result The test URL responded within the timeout through that proxy. Long-lived sessions or every target domain will work.
Current group member Which proxy new connections should use. Existing connections were migrated.
Connection list Which hosts the core is currently handling and which rules matched. Traffic from applications that bypass Clash entirely.
Core version Which controller implementation is responding. That the active profile has the features your script expects.

For repeatable results, test a small set of candidate nodes rather than every node in a large subscription. Provider lists can contain hundreds of entries, and testing all of them creates unnecessary traffic, fills logs, and may trigger provider-side rate limits. Exclude obvious names such as expired, maintenance, or overloaded nodes when your provider uses those conventions. Keep the candidate list explicit when the script controls a production workload.

Switch a Proxy Group Programmatically Under the Hood

After selecting a candidate, send a request to the group endpoint using the HTTP method expected by the compatible controller, commonly PUT. The JSON body identifies the member to select. The group name belongs in the URL path, while the selected proxy is sent in the request body. Both values must be taken from the live /proxies response, not from a hard-coded assumption that may become stale after a subscription update.

curl -s -X PUT \
  -H "Authorization: Bearer $CLASH_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"name":"Node A"}' \
  "http://127.0.0.1:9090/proxies/PROXY"

In a real script, add a verification step after the update. Query the same group and compare its now field with the requested member. If the value differs, the group may be read-only, the selected name may no longer exist, or the request may have reached a different Clash instance. Treat a failed verification as a failed switch rather than continuing silently.

current=$(curl -s \
  -H "Authorization: Bearer $CLASH_API_SECRET" \
  http://127.0.0.1:9090/proxies/PROXY | jq -r '.now')

if [ "$current" != "Node A" ]; then
  echo "Proxy switch was not confirmed" >&2
  exit 1
fi

Use a cooldown and a hysteresis margin. For example, switch only when the current node fails two consecutive checks, or when a candidate is at least 150 milliseconds faster and has passed two checks separated by a delay. This prevents flapping when two nodes alternate between nearly identical latency results. A switch should also be suppressed while a sensitive operation is active, such as a database migration, a large upload, or a WebSocket session that cannot reconnect cleanly.

Remember that selecting a proxy group is not the same as changing the rule that chooses the group. If your rules route a domain to AI, changing PROXY may have no effect on that domain. First inspect the group actually referenced by the matched rule. If a connection log shows AI or Streaming, switching the generic PROXY group will not repair the request. This is one of the most common reasons an API automation script appears to work while application traffic remains unchanged.

For a scheduled job, keep the workflow small and observable:

  • Read the controller URL and secret from protected configuration.
  • Fetch the target group and validate that it is a selectable group.
  • Build a candidate list from the group’s live members.
  • Run bounded delay tests with a fixed test URL.
  • Apply failure thresholds, cooldowns, and a minimum improvement margin.
  • Switch only when the selected candidate differs from the current member.
  • Verify the group state and write a redacted result to the log.

Scheduling, Observability, and Production Troubleshooting Explained

On Linux or macOS, a timer, cron job, or system service can run the health-check script. On Windows, Task Scheduler can start a PowerShell script at login or at a fixed interval. A five-minute interval is usually a better starting point than a thirty-second loop. The objective is recovery from a meaningful failure, not constant optimization. If your provider already supplies a url-test or fallback group, compare your external script against that native behaviour before adding another decision layer.

Make the script idempotent. Running it twice with the same healthy result should not repeatedly issue switch requests. Include a lock so two scheduled instances cannot test and switch concurrently. Store the last selected node and the last failure timestamp if you need persistence across restarts, but do not store the controller secret beside those files with broad permissions. Output timestamps, group names, selected node names, latency values, and error categories; omit bearer tokens and subscription URLs.

When a request returns an error, classify it before changing the configuration. A connection refusal usually means the core is stopped, the port is wrong, or the controller is bound to another address. A 401 or 403 points to an absent or incorrect secret. A 404 may indicate an unsupported endpoint, a wrongly encoded proxy name, or a controller implementation with different API semantics. A 400 often means the group or proxy name is invalid, the JSON body is malformed, or the selected member is not valid for that group.

Do not use automatic switching to hide a broken capture path. If the target application bypasses the Clash mixed port and TUN mode is disabled, changing nodes through the controller cannot affect that application. Confirm the process appears in the connection view, check the matched rule, and verify that DNS mode and routing mode are consistent before tuning the selection algorithm.

For deeper diagnosis, query the active connections endpoint and inspect the live dashboard at the same time. Look for the destination hostname, rule, proxy chain, upload and download counters, and connection state. If the application reports a timeout but no connection appears in Clash, the problem is outside the controller workflow: the application may ignore system proxy settings, use its own DNS resolver, or require TUN capture. If a connection appears as DIRECT when you expected a group, fix rule order before writing more automation.

DNS deserves special attention. With fake-IP or redirection modes, the hostname visible to the rule engine and the address visible to the application may differ. A health-check script that tests a URL through the controller does not automatically prove that the application’s DNS request follows the same path. Keep the test domain representative of the service you care about, and compare the controller result with a real request made through the application’s actual proxy or TUN path.

Finally, plan for upgrades. Older Clash-compatible cores may expose fewer endpoints or return different JSON fields than current Mihomo builds. Pin your script to the fields it truly needs, validate responses, and test after changing clients or importing a new profile. A robust external-controller integration is not just a successful curl command; it is a protected control plane with bounded tests, verified state, clear logs, and a fallback that leaves the last known-good node in place.

Compared with GUI-only clients, lightweight node-switching scripts offer repeatability but can become fragile when endpoint names, authentication rules, and core versions differ; browser extensions and provider dashboards often lack reliable group control, while custom scripts tend to provide little guidance when a rule or DNS path is wrong. Clash is more practical in this scenario because its Clash-focused documentation connects API control with real client settings, proxy groups, TUN capture, and troubleshooting steps instead of treating the controller as an isolated command list. If you want a supported starting point for testing these workflows across modern Clash-compatible clients, download Clash and build your automation around a verified local controller rather than guessing at ports and endpoints.