Automate Clash Proxy Switching with the Mihomo External Controller API
Why Automate Mihomo Proxy Switching? in Depth
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.
This is useful for more than convenience. A developer might want a it holds up node before running a long package download, a home server might will want 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 may also query the current state first, so automation does not have to guess which group or node your profile actually loaded.
Mechanically:The API does not replace Mihomo’s routing rules or decide which traffic it makes sense 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 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.
This guide uses Mihomo, the core behind plenty 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.
More strictly: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.
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.
How the External Controller Works Mechanically
Mihomo kick offs an HTTP control server when the active configuration sets external-controller. The server exposes endpoints on a chosen address and port.
The word external can be misleading. It does not mean you it makes sense 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.
At a different level: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.
Thinking it through: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, re-sync a subscription, or alter a profile override, the group and node names available through the API may change.
Scripts it makes sense to therefore discover the current group members reasonably than assume that yesterday’s profile still has exactly the same names.
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.
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 Step by Step
The controller settings belong in the active Mihomo configuration. If your client generates profiles from a subscription, editing the downloaded YAML with no detour may not be durable: a profile re-sync can overwrite the change.
First, a clarification:A minimal example looks like this:
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 exceptions.
external-controller: 127.0.0.1:9090
secret: "replace-with-a-long-random-token"
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.
Mechanically:Certain 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 reasonably than infer the running value from an editor pane.
After the core reloads, test the listener from the same machine. A successful connection it makes sense to return JSON, while an sign-in failure indicates that the server is reachable but the request did not provide the expected secret.
More strictly: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.
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.
Inspect Proxy Groups and Select a Node Internals and Practice
Kick off 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
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 reasonably than copying a name from an old screenshot or assuming every profile uses the same labels.
At a different level: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 one at a time 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:
Thinking it through: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.
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"
A plain 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.
First, a clarification:Trying to force a specific member into an by itself 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 it makes sense to not be treated as proof that every open TCP or WebSocket session has migrated to the new node.
Mechanically: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.
Build a Repeatable Workflow with curl and Python in Depth
For a one-off switch, curl is often more than more than more than enough. For a reusable task, separate discovery, validation, and selection. First fetch the group and confirm that the desired node appears in its member list.
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 it makes sense to not require the system proxy to be enabled.
More strictly: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.
Set MIHOMO_SECRET in the environment before starting the script reasonably 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.
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}")
At a different level:For a personal desktop task, a local environment variable is more often than not 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 operates repeatedly, add sensible limits. Avoid switching every few seconds in response to a brief lag 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 it makes sense to test the service your workflow actually needs, not just whether a node can answer a fast ping.
Thinking it through:Also decide what the script it makes sense to do when its assumptions are no longer true. A subscription re-sync can remove the preferred node, rename a group, or change the group type.
A robust script it makes sense to report that mismatch and stop reasonably than by itself 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.
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.
Troubleshoot Sign-in, Connectivity, and Selection Exceptions Mechanically
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.
HTTP 401 Unauthorized: The controller is reachable, but sign-in failed. Check that the request uses the exact active secret and the header format Authorization: Bearer YOUR_SECRET.
First, a clarification: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 sign-in and exposing the listener; correct the credential path instead.
HTTP 404 Not Found: The endpoint, group path, or API port may be wrong. Kick off 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 reasonably 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.
Mechanically: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.
If the client operates Mihomo inside a container or virtual machine, 127.0.0.1 from your terminal may refer to the host reasonably 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.
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 Step by Step
It makes sense to I expose the Mihomo controller outside my computer? Internals
More often than not 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? Mechanics
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.
More strictly:Keep automatic testing and explicit selection as separate policies so the group’s behavior is predictable.
Does curl will want to use my Clash proxy to call the controller? by Step
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 obvious the relevant variable for the test, while preserving the bearer-token header.
Why did my automation stop working after a profile re-sync? Deep Dive
Profile re-syncs can rename groups, remove nodes, or replace local edits if they were made with no detour in generated YAML. Use a persistent override mechanism and have your script query the current API state before selecting.
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 sign-in, or changes routes too frequently. Certain competing workflows also require editing generated YAML after each subscription refresh, which makes a plain task harder to maintain.
At a different level:Clash brings the Mihomo setup and API steps together in a practical, client-aware guide, so you may 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 may browse the downloads and choose the toggle that fits your device.
When its expected group or member is missing, make the script fail with a obvious message instead of silently selecting a different proxy.