What the External Controller API Does
Clash’s external controller API provides an HTTP interface for monitoring and controlling a running Clash or Mihomo core. Instead of opening the client interface and changing a proxy group manually, you can send a request that selects a node, reads the current connection state, measures delay, or changes the active mode. This makes the API useful for repeatable automation rather than one-time adjustments.
In practical terms, the API lets a local script communicate with the Clash process through an address such as 127.0.0.1:9090. A desktop client may expose this controller automatically, while other clients require you to enable it in the YAML profile. The exact interface can vary slightly between Clash variants, Clash Verge Rev, and Mihomo, but the core workflow remains similar: enable the controller, protect it with a secret, discover the available proxy groups, test candidate nodes, and send a selection request.
This guide focuses on four useful tasks: checking node latency, choosing a node according to a measurable rule, switching on a schedule, and recovering when the selected node stops responding. The examples use YAML, Shell, and Python so that you can start with a simple command and later build a more reliable automation tool.
Automation Goal
Keep a designated Clash proxy group on a responsive node without exposing the controller to your local network or blindly switching whenever a single probe is slow.
1Enable and Secure the Controller
The controller is configured in the main YAML profile, usually under the external-controller and secret keys. A safe local-only configuration binds the API to the loopback address. This means that programs running on the same computer can use it, while other devices on the Wi-Fi network cannot connect directly.
If your client already provides an API address in its settings panel, do not create a second controller entry with a different port. First confirm which core is active and which port it is listening on. Clash Verge Rev and Mihomo-based clients may show the controller URL under general settings, developer settings, or an API-related panel. After editing a profile, reload the profile or restart the core so the new values take effect.
Why the Secret Matters
The external controller can change proxy selections and, depending on the implementation, access connection information. Leaving the secret empty is convenient for a quick local test but is a poor permanent setting. Use a long random value and keep it out of public repositories, screenshots, shared shell history, and web pages. A controller bound to 0.0.0.0 is especially risky because it may become reachable from every interface. Only use a non-loopback bind address when you understand the firewall and authentication implications.
For a quick health check, send a request to the version endpoint. The following command uses the common Authorization header format:
A JSON response containing core and version information confirms that the endpoint is reachable and that authentication succeeded. If you receive connection refused, verify the port and restart the core. A 401 response usually means that the secret is missing or does not match. If the endpoint responds but later proxy requests fail, check the group name and node names returned by the API rather than guessing them from the subscription file.
Security Warning
Do not forward port 9090 through your router, expose it through a public reverse proxy, or place the secret in client-side JavaScript. The controller should normally remain a local administrative interface.
2Discover Groups and Test Candidate Nodes
Automation becomes reliable only when it uses the names reported by the running core. Subscription YAML may contain a group called Proxy, 🚀 Proxy, or a provider-specific name, and node labels may include spaces, emoji, region codes, or duplicate suffixes. Query the groups endpoint before writing a switching script.
The response contains both proxy groups and individual proxies. A group normally has a type such as Selector, URLTest, Fallback, or LoadBalance. It also exposes a now field for the currently selected member and a all array containing available members. Read these values directly so your script can ignore DIRECT, REJECT, nested groups, and unavailable entries when appropriate.
Latency Is a Tool, Not a Guarantee
The delay endpoint is commonly used to test a particular proxy through a URL such as /proxies/{name}/delay. Because node names must be URL encoded, use a command-line or programming-language encoder instead of placing the raw name into a URL. A typical request looks like this:
Use a small, stable probe URL and a timeout that reflects your connection. A five-second timeout is reasonable for an interactive check, while a scheduled job may use three to eight seconds depending on the network. Do not compare measurements taken from different probe URLs as if they were identical. A node can reach a lightweight static resource quickly while still performing poorly with video, large downloads, or applications that require UDP.
Before selecting the lowest number, apply sensible filters:
- Reject failed probes: Treat timeouts, connection errors, and malformed JSON as unavailable rather than as zero latency.
- Use a threshold: A node under 250 milliseconds may be acceptable for browsing, while a higher value may still work for downloads but feel poor for interactive applications.
- Consider stability: Run two or three probes and use the median. One unusually fast result should not immediately replace a stable node.
- Preserve policy: Avoid selecting a node that violates your location, service, or account requirements merely because its delay is lower.
- Keep exclusions explicit: Ignore group names, relay entries,
DIRECT, andREJECTunless your script intentionally supports them.
Pro Tip: Test the Active Path
A proxy node’s controller delay is only one signal. For important workflows, combine it with an application-level request through the selected group and monitor errors over time.
3Build an Automatic Switching Workflow
The API request that changes a selector is a PUT request to the group endpoint. The JSON body usually contains the exact member name in a name property. The group name and selected node must be URL encoded because either can contain spaces or non-ASCII characters.
A production script should not switch on every successful probe. First read the group’s current now value, compare it with the preferred candidate, and require a meaningful improvement. For example, switch only when the candidate is at least 30 milliseconds faster, or when the current node has failed two consecutive checks. This hysteresis prevents rapid oscillation when two nodes have similar latency.
Practical Python Example
The following example discovers a selector, tests its members, and selects the fastest responsive candidate. It intentionally excludes special entries and nested groups. Adjust the group name, probe URL, and timeout for your own profile.
Install the required dependency with python3 -m pip install requests, then store the script with restricted file permissions if it contains the controller secret. On Linux or macOS, run it manually first and inspect the output. On Windows, the same logic can run from PowerShell or Task Scheduler if Python is installed. For a long-running service, move the secret into an environment variable rather than keeping it in the source file.
Scheduled and Failover Modes
For scheduled switching, run the script through a system scheduler instead of creating an endless loop that consumes resources. A Linux cron entry might execute every ten minutes:
On Windows, create a Task Scheduler task that starts the script at logon and repeats it at a fixed interval. Use a sufficiently long interval to avoid rate-limiting the controller and to give each node time to settle. A five-minute schedule is usually more useful than a five-second loop.
Failover should be more conservative than ordinary optimization. Keep a record of consecutive failures for the active node. After one failed request, retry with a short delay. After two or three failures, test the candidate list and select the best healthy node. If every candidate fails, do not continuously rewrite the group; retain the current state, log the event, and notify the user if the connection is business-critical. This avoids turning a temporary upstream outage into constant switching noise.
- Probe the current node before replacing it.
- Use a median from multiple samples instead of one result.
- Require a latency improvement margin to prevent flapping.
- Record the timestamp, node, delay, and error reason.
- Apply a cooldown after switching so the new node can stabilize.
- Never treat an empty candidate list as permission to select an arbitrary entry.
4Troubleshooting, Compatibility, and Safe Operation
Different Clash clients may expose different controller capabilities. Mihomo generally provides a broad API surface, while older cores or lightweight mobile clients may omit endpoints or use slightly different response fields. If a request documented for one core returns 404, check that core’s API documentation and inspect the actual JSON returned by /proxies. Do not assume that every group supports manual selection: a URLTest or Fallback group may manage its member automatically.
Common errors have straightforward causes. A 401 Unauthorized response indicates a secret problem. A 404 Not Found often means that the group or node name was not encoded correctly, or that the endpoint is unavailable in the current core. A 400 Bad Request usually points to an invalid JSON body or an attempt to select a member that is not present in the group. A timeout from the delay endpoint does not necessarily mean the controller is broken; it may indicate that the target node or test URL is unreachable.
YAML also deserves careful attention. Indentation must use spaces, not tabs, and the controller keys must be placed at the top level. Quote secrets containing punctuation, and avoid accidentally including a trailing comment inside the value. After changing a profile, confirm that the client did not overwrite your manual edits during the next subscription update. If the provider regenerates the complete YAML, place stable overrides in the client’s profile or merge mechanism instead of editing a temporary generated file.
- Check reachability: Confirm the controller port with the client settings and test
/version. - Inspect names: Print the exact group and node names returned from
/proxies. - Encode every path segment: Spaces, emoji, slashes, and non-Latin characters require URL encoding.
- Review logs: Keep enough information to identify failures without writing the secret or sensitive subscription URL.
- Limit permissions: Restrict script and log-file access on shared computers.
- Test manually first: Make one API change by hand before enabling a scheduled job.
Finally, remember that automatic node selection is an optimization layer, not a substitute for a sound Clash profile. DNS mode, routing rules, UDP support, TUN settings, and the quality of the proxy provider all affect the final experience. A low controller delay cannot fix an incorrect rule that sends traffic to DIRECT, nor can it guarantee that a service will accept a particular exit IP. Measure the behavior that matters, set reasonable thresholds, and favor stable connectivity over a constantly changing “fastest” label.
Frequently Asked Questions
Can I use the API without a secret?
Some local configurations permit an empty secret, but this is not recommended for a permanent setup. The API can change active proxy selections, so always configure a strong secret and send it through the authorization header. Keep the listener on 127.0.0.1 unless there is a specific, controlled reason to allow remote access.
Why does my script fail when a node name contains spaces?
Node and group names are URL path segments, not plain text. Encode them with a proper URL encoder before constructing the endpoint. The JSON body still needs the original name, so encode the URL separately and send the unmodified node label in the name field.
Is the lowest delay always the best node?
No. Delay is only one measurement and may vary because of congestion, probe location, routing, or temporary packet loss. Use multiple samples, a stability threshold, and application-level checks when necessary. A slightly slower node with fewer failures is usually better than a node that wins one isolated test and then disconnects.
Should I switch nodes every few seconds?
No. Frequent switching can interrupt existing connections, create unstable sessions, and make troubleshooting difficult. Use a cooldown, require a meaningful improvement, and schedule checks at a practical interval such as five or ten minutes. For failover, switch only after repeated failures and return to normal optimization after the connection has stabilized.
Once the controller is secured and your selection rules are conservative, the API becomes a dependable extension of Clash rather than a fragile collection of one-line commands. Start with manual requests, validate the returned group data, then add latency checks, scheduling, logging, and failover one layer at a time.