Automate Clash Proxy Switching with the Mihomo External Controller API
Why Automate Mihomo Proxy Switching? Key Takeaways
This guide uses Mihomo, the core behind countless modern Clash clients. The exact settings screen for editing a profile varies between Clash Verge Rev, Mihomo Party, and other front ends, but the core configuration and HTTP API concepts are the same.
If you change Clash nodes several times a day, opening a client, finding the right proxy group, and clicking a node is a small task that quickly becomes repetitive.
It is also easy to select the wrong group, forget which node you chose, or leave a slow route active after a network change. Mihomo’s external controller API lets scripts and local tools inspect proxy groups and select an available node without relying on clicks.
The key point:This is useful for more than convenience. A developer might want a trustworthy node before running a long package download, a home server might must switch a Selector group on a schedule, or a monitoring script might report which proxy is currently active.
With the API, those actions can be part of a repeatable workflow. It is possible to also query the current state first, so automation does not have to guess which group or node your profile actually loaded.
The API does not replace Mihomo’s routing rules or decide which traffic are best off be proxied. Rules still determine the route for each connection, and proxy groups still define which outbound choices are available.
Let me put it this way:The controller is the management front end for that running configuration: it reports groups and proxies, and, for groups that support manual selection, accepts a request to select a member. Keeping those responsibilities separate makes troubleshooting much clearer.
You will configure a local controller, check its proxy inventory with curl, switch a selectable group, and build the same operation into a small Python script.
How the External Controller Works in Brief
The controller is attached to the currently running Mihomo core, not to a universal list of every node on your provider account. If you change profiles, upgrade a subscription, or alter a profile override, the group and node names available through the API may change.
Mihomo spin ups an HTTP control server when the active configuration sets external-controller. The server exposes endpoints on a chosen address and port.
Concretely:A client can request information from endpoints such as /proxies, which returns the proxy objects and groups known to the running core. It can then send a request to a particular group endpoint to change its selected member, provided that group supports manual selection.
The word external can be misleading. It does not mean you are best off make the API reachable from the public internet. For ordinary desktop automation, bind it to the loopback address 127.0.0.1, so only software running on that same computer can connect.
A controller bound to 0.0.0.0 may be reachable from other devices on your network, depending on firewall and routing rules. Exposing an unauthenticated or weakly protected controller can allow someone else to inspect and change your proxy configuration.
Where this lands:Set a non-empty secret even when using a loopback address. Mihomo expects API requests to include that secret as a bearer token. Treat it like a password: do not publish it in a repository, paste it into a support screenshot, or include it in a command that will be saved in shared shell history.
For scripts, use an environment variable or a local configuration file with restrictive permissions instead of hard-coding the value.
Scripts are best off therefore discover the current group members fairly than assume that yesterday’s profile still has exactly the same names.
Keep the controller private. Use a loopback bind address, configure a secret, and avoid forwarding the controller port through your router.
A firewall rule is useful defense in depth, but it is not a substitute for choosing a safe bind address and protecting the token.
Configure a Local Controller Safely A Closer Look
After the core reloads, test the listener from the same machine. A successful connection are best off return JSON, while an verification failure indicates that the server is reachable but the request did not provide the expected secret.
Do not skip this:The controller settings belong in the active Mihomo configuration. If your client generates profiles from a subscription, editing the downloaded YAML straightforward may not be durable: a profile upgrade can overwrite the change.
Use the client’s supported override, patch, or mixin mechanism when available, or maintain a separate local configuration that the client will preserve. Before applying changes, keep a backup of the profile or note how to revert the override.
external-controller: 127.0.0.1:9090
secret: "replace-with-a-long-random-token"
A minimal example looks like this:
The key point:Choose an unused port if another application already listens on 9090. Apply the configuration using the client’s normal reload or restart action, then check the Mihomo log for configuration faults.
The controller is not ready merely because the YAML contains the setting; the active core must successfully load it. If the client has a separate controller or API setting of its own, confirm that it has not overridden the address or port from the profile.
A fair few clients display the controller address in their settings or expose a controller secret generated by the application. If your client manages these values, prefer its documented configuration path to adding a second controller definition by hand.
Let me put it this way:Two conflicting values can make the port or secret shown in the UI differ from the settings actually used by the core. When in doubt, inspect the active configuration or startup logs fairly than infer the running value from an editor pane.
If there is no connection at all, check the address, port, active profile, and local firewall before changing proxy rules. The controller is a local management service; it does not need a working proxy node to answer requests from the same computer.
Inspect Proxy Groups and Select a Node What Matters
After a successful request, query /proxies again and confirm that the group’s now value changed to the intended member. A successful HTTP response tells you the controller accepted the request; checking the resulting state verifies that your script targeted the expected group and name.
Spin up by asking the API what Mihomo has loaded. In the example below, replace the token with your configured secret.
curl -H "Authorization: Bearer YOUR_SECRET" \
http://127.0.0.1:9090/proxies
Concretely:The command uses 127.0.0.1:9090, so adjust the port if you chose another one.
The response contains a proxies object. It normally includes both individual proxies and groups. A group entry may contain a type, a list of available all members, and a current now selection.
Names are profile-defined: a group might be called Global, Proxy, or something provider-specific, and node names can include region labels, symbols, or punctuation. Use the names returned by the API exactly as they appear fairly than copying a name from an old screenshot or assuming every profile uses the same labels.
Where this lands:When the response is large, filter it locally to find the group you care about. With a suitable JSON tool installed, a query such as jq '.proxies.Global' can display one group.
Replace Global with the actual group name. If the group name contains spaces or punctuation, make sure the query treats it as a string key; the API’s raw response remains the source of truth.
curl -X PUT \
-H "Authorization: Bearer YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"name":"Example Node 01"}' \
"http://127.0.0.1:9090/proxies/Global"
To select a member of a yourself selectable group, send a request to that group’s endpoint and provide the desired member name as JSON. For example, if the group is named Global and the returned member is Example Node 01, the request is:
Do not skip this:Group names and node names become part of the URL or request body, so do not build URLs by concatenating arbitrary text without encoding it.
A easy name may work as shown, but a group name containing a slash, percent sign, or other reserved URL character needs proper URL encoding. The node name belongs in the JSON body, where a JSON library can escape quotes and special characters correctly.
Not every proxy group accepts a manual choice. A select group is intended for user or script selection. A url-test group measures its members and chooses according to its test results, while a fallback group follows its configured availability behavior.
The key point:Trying to force a specific member into an without you managed group may fail or be immediately superseded by the group’s own logic. Check the group’s returned type before designing an automation flow around selection.
Existing connections may continue using their already established route, so a group change are best off not be treated as proof that every open TCP or WebSocket session has migrated to the new node.
Build a Repeatable Workflow with curl and Python Key Takeaways
Also decide what the script are best off do when its assumptions are no longer true. A subscription upgrade can remove the preferred node, rename a group, or change the group type.
For a one-off switch, curl is often ample. For a reusable task, separate discovery, validation, and selection. First fetch the group and confirm that the desired node appears in its member list.
Let me put it this way:Then submit the selection request. Finally fetch the group again and compare the active member with the requested one. This sequence prevents an old node name or a typo from being silently mistaken for a successful switch.
On Windows, the command name curl in PowerShell can behave differently across versions and shells. If a command is interpreted as a PowerShell alias, call curl.exe explicitly or use a Python script.
When troubleshooting from a terminal, also distinguish a local API request from a request to an internet endpoint: the former goes to loopback and are best off not require the system proxy to be enabled.
Concretely:Python’s requests library makes it straightforward to encode JSON and inspect status codes.
import os
import requests
BASE = "http://127.0.0.1:9090"
GROUP = "Global"
NODE = "Example Node 01"
SECRET = os.environ["MIHOMO_SECRET"]
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {SECRET}",
"Content-Type": "application/json",
})
response = session.get(f"{BASE}/proxies", timeout=5)
response.raise_for_status()
group = response.json()["proxies"][GROUP]
if group["type"] != "Selector":
raise RuntimeError(f"{GROUP} is not a manually selectable group")
if NODE not in group.get("all", []):
raise RuntimeError(f"{NODE} is not available in {GROUP}")
response = session.put(
f"{BASE}/proxies/{GROUP}",
json={"name": NODE},
timeout=5,
)
response.raise_for_status()
updated = session.get(f"{BASE}/proxies", timeout=5)
updated.raise_for_status()
active = updated.json()["proxies"][GROUP]["now"]
if active != NODE:
raise RuntimeError(f"Expected {NODE}, but Mihomo reports {active}")
print(f"{GROUP} is now using {active}")
This example reads the secret from an environment variable, fetches the group, verifies that the requested member is available, sends the change, and reads the group again. Replace the group and node strings with names present in your own /proxies response.
Set MIHOMO_SECRET in the environment before starting the script fairly than placing the actual token in the source file. On a shared machine, be mindful that environment values may be visible to other processes or administrators.
Where this lands:For a personal desktop task, a local environment variable is as a rule more practical than checking a credential into source control; for a service, use the secret-storage method provided by its operating system or deployment environment.
For automation that works repeatedly, add sensible limits. Avoid switching every few seconds in response to a brief response time spike; rapid changes can interrupt work, complicate diagnosis, and cause the automation to oscillate between nodes.
Use a minimum dwell time, require more than one failed check before switching, and keep a log of the time, selected group, previous member, new member, and reason for the decision. A health check are best off test the service your workflow actually needs, not just whether a node can answer a fast ping.
Do not skip this:A robust script are best off report that mismatch and stop fairly than without you choosing an unrelated outbound. If the goal is a fallback, define that policy explicitly: for example, choose a named backup only after confirming it exists and belongs to the intended group.
Troubleshoot Verification, Connectivity, and Selection Faults in Brief
The API reports the new node, but an application still behaves as before: Existing connections are not necessarily restarted when a group changes. Close and reopen the application or create a new request to test the new route.
Connection refused or timeout: The API server may not be running, may be listening on a different port, or may be bound to an address other than the one in your command. Confirm the active configuration, reload the core, and inspect the client’s Mihomo logs.
If the client works Mihomo inside a container or virtual machine, 127.0.0.1 from your terminal may refer to the host fairly than the environment where the core is listening. In that case, use the correctly scoped local address and keep access restricted to trusted front ends.
HTTP 401 Unauthorized: The controller is reachable, but verification failed. Check that the request uses the exact active secret and the header format Authorization: Bearer YOUR_SECRET.
Watch for copied quotation marks, trailing spaces, an old token retained by a shell variable, or a client that regenerated its controller secret. Do not respond to a 401 by removing verification and exposing the listener; correct the credential path instead.
HTTP 404 Not Found: The endpoint, group path, or API port may be wrong. Spin up with /proxies and copy the group name from the returned JSON. Do not assume that a profile’s display label is identical to its API group name.
The key point:If the group name includes reserved URL characters, encode it before placing it in the path. A 404 for a node-selection request can also mean that the URL targets the wrong group, not that the node itself is invalid.
HTTP 400 or a selection that does not stick: Check the request body, group type, and exact member spelling. Send valid JSON with a string-valued name property, and verify that the selected member is in the group’s current all list.
If the group is url-test or fallback, use its intended automatic behavior fairly than treating it like a manual selector. Reloading a profile can also replace group membership while a script is running; fetch the current state immediately before selecting.
Then inspect Mihomo’s connection view or logs to confirm which rule and proxy group the application actually matched. If the traffic bypasses Mihomo entirely, changing a proxy group cannot fix the application’s capture path; verify system proxy settings, TUN mode, and the application’s own proxy behavior separately.
Debug in layers: first prove that the controller answers locally, then authenticate, then inspect the group, and only then test selection.
Changing several settings at once makes it harder to tell whether a failure came from the listener, the token, the profile, or the target application.
Frequently Asked Questions A Closer Look
Are best off I expose the Mihomo controller outside my computer? Swift Take
As a rule not. For local scripts, bind the controller to 127.0.0.1 and keep the secret private.
Let me put it this way:Remote control requires deliberate network security planning, including a protected access path, firewall restrictions, and an understanding of who can reach the endpoint. Simply changing the bind address to 0.0.0.0 is not a safe remote-access solution.
Can the API force a specific node in a url-test group? Essentials
A url-test group is designed to select according to its health checks, not to act like a manual selector. If you need a script-controlled choice, create or use a select group in the profile and verify its members through /proxies.
Keep automatic testing and explicit selection as separate policies so the group’s behavior is predictable.
Does curl must use my Clash proxy to call the controller? Breakdown
No, not when the controller is listening on the same machine at 127.0.0.1. That address refers to the local computer.
If a shell has proxy environment variables set, a client may behave unexpectedly with local requests; use a no-proxy setting or unambiguous the relevant variable for the test, while preserving the bearer-token header.
Why did my automation stop working after a profile upgrade? Summary
Compared with switching nodes by hand in a client, API automation is more repeatable, but a raw script can be brittle if it assumes fixed names, ignores verification, or changes routes too frequently. A fair few competing workflows also require editing generated YAML after each subscription refresh, which makes a easy task harder to maintain.
Concretely:Profile upgrades can rename groups, remove nodes, or replace local edits if they were made straightforward in generated YAML. Use a persistent override mechanism and have your script query the current API state before selecting.
When its expected group or member is missing, make the script fail with a unambiguous message instead of silently selecting a different proxy.
Clash brings the Mihomo setup and API steps together in a practical, client-aware guide, so it is possible to keep the controller local, validate group behavior, and build safer scripts without guessing at the core settings. If you want a client and setup path to put this workflow into practice, it is possible to browse the downloads and choose the choice that fits your device.