Automate Clash Proxy Switching with the Mihomo External Controller API
Why Automate Mihomo Proxy Switching? from Practice
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.
This is useful for more than convenience. A developer might want a solid node before running a long package download, a home server might are expected to 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. You get 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 will want to be proxied. Rules still determine the route for each connection, and proxy groups still define which outbound choices are available.
The controller is the management control panel 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.
This guide uses Mihomo, the core behind a good number of modern Clash clients. The exact settings screen for editing a profile varies between Clash Verge Rev, Mihomo Party, and other control panels, but the core configuration and HTTP API concepts are the same.
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 Lessons Learned
Mihomo bring ups an HTTP control server when the active configuration sets external-controller. The server exposes endpoints on a chosen address and port.
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 will want to 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.
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.
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, pull the latest a subscription, or alter a profile override, the group and node names available through the API may change.
Scripts will want to therefore discover the current group members a bit 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 in the Wild
The controller settings belong in the active Mihomo configuration. If your client generates profiles from a subscription, editing the downloaded YAML right off may not be durable: a profile pull the latest 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.
A minimal example looks like this:
external-controller: 127.0.0.1:9090
secret: "replace-with-a-long-random-token"
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 problem messages.
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 handful of 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.
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 a bit than infer the running value from an editor pane.
After the core reloads, test the listener from the same machine. A successful connection will want to return JSON, while an credential check failure indicates that the server is reachable but the request did not provide the expected secret.
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 Hands-On Notes
Bring up by asking the API what Mihomo has loaded. In the example below, replace the token with your configured secret. The command uses 127.0.0.1:9090, so adjust the port if you chose another one.
curl -H "Authorization: Bearer YOUR_SECRET" \
http://127.0.0.1:9090/proxies
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 a bit than copying a name from an old screenshot or assuming every profile uses the same labels.
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.
To select a member of a step by step 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:
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"
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 uncomplicated 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.
Trying to force a specific member into an hands-free 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.
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.
Existing connections may continue using their already established route, so a group change will want to 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 from Practice
For a one-off switch, curl is often plenty. For a reusable task, separate discovery, validation, and selection. First fetch the group and confirm that the desired node appears in its member list.
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 will want to not require the system proxy to be enabled.
Python’s requests library makes it straightforward to encode JSON and inspect status codes.
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.
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}")
Set MIHOMO_SECRET in the environment before starting the script a bit 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.
For a personal desktop task, a local environment variable is by built-in 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 be up and runnings repeatedly, add sensible limits. Avoid switching every few seconds in response to a brief ping 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 will want to test the service your workflow actually needs, not just whether a node can answer a fast ping.
Also decide what the script will want to do when its assumptions are no longer true. A subscription pull the latest can remove the preferred node, rename a group, or change the group type.
A robust script will want to report that mismatch and stop a bit than hands-free 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 Credential check, Connectivity, and Selection Problem messages Lessons Learned
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 be up and runnings Mihomo inside a container or virtual machine, 127.0.0.1 from your terminal may refer to the host a bit than the environment where the core is listening. In that case, use the correctly scoped local address and keep access restricted to trusted control panels.
HTTP 401 Unauthorized: The controller is reachable, but credential check 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 credential check and exposing the listener; correct the credential path instead.
HTTP 404 Not Found: The endpoint, group path, or API port may be wrong. Bring 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.
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 a bit 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.
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.
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 in the Wild
Will want to I expose the Mihomo controller outside my computer? in Practice
By built-in not. For local scripts, bind the controller to 127.0.0.1 and keep the secret private.
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? From Experience
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 are expected to use my Clash proxy to call the controller? Field Notes
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 easy to follow the relevant variable for the test, while preserving the bearer-token header.
Why did my automation stop working after a profile pull the latest? Tips
Profile pull the latests can rename groups, remove nodes, or replace local edits if they were made right off 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 easy to follow message instead of silently selecting a different proxy.
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 credential check, or changes routes too frequently. A handful of competing workflows also require editing generated YAML after each subscription refresh, which makes a uncomplicated task harder to maintain.
Clash brings the Mihomo setup and API steps together in a practical, client-aware guide, so you get 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, you get to browse the downloads and choose the entry that fits your device.