Clash External Controller API: Advanced Node Switching Guide

What the Clash External Controller Actually Controls in Depth

The Clash external-controller API is a local HTTP front-end 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.

This is different from changing a YAML profile on disk. The API operates on the running core and more often than not 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 it makes sense to distinguish between a temporary switch and a persistent configuration change.

Mechanically: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. Certain 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.

The controller is full-featured because it exposes live state reasonably 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 lag tests, a script can make a decision based on current reachability.

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.

More strictly:This makes the API useful for developers running long downloads, CI jobs, terminal tools, media workflows, or services that will want to recover from a bad node without requiring a person to watch the Clash dashboard.

Instead of one at a time clicking through the proxy page, you may query the active configuration, list available nodes, test lag, select a member inside a proxy group, inspect live connections, and build an automatic node-switching workflow around those operations.

Enable and Secure the Controller Before Automating It Mechanically

Kick off 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 toggle.

Sign-in 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:

At a different level: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 sign-in.

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

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.

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.

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

Thinking it through: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 everything included request headers when debugging CI logs.

Before continuing, verify that the API port is reachable and that the JSON response is from the expected Clash instance. Multiple clients can operate on one machine, especially after testing Clash for Windows, Clash Verge Rev, or Mihomo Party.

$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

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.

First, a clarification:Prefer a loopback binding such as 127.0.0.1:9090 when the automation operates on the same computer. Avoid binding to 0.0.0.0 unless you have a obvious remote-management requirement and a firewall policy that limits access to a trusted network.

Inspect Proxy Groups and Test Nodes Safely Step by Step

The most of of useful discovery endpoint is more often than not /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.

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.

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

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 one at a time concatenating unsafe characters.

Mechanically:Lag 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 trim 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.

A practical health check it makes sense to combine several signals. First, verify that the node exists in the current response. Second, measure lag with a timeout that reflects the use case. Third, reject HTTP exceptions, timeouts, and implausibly unstable results.

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"

More strictly:Fourth, apply a policy that prevents a node with slightly better lag from replacing a not jumpy node every few seconds. This last point matters because automatic switching can create more disruption than a slow but it holds up connection.

For repeatable results, test a small set of candidate nodes reasonably 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.

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 it makes sense to 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.

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.

At a different level:The exact response shape can vary between legacy Clash cores and newer Mihomo releases, so scripts it makes sense to check for missing fields reasonably than assuming every proxy is a selectable node.

Switch a Proxy Group Programmatically Internals and Practice

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.

In a real script, add a verification step after the re-sync. Query the same group and compare its now field with the requested member.

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"

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 reasonably than continuing silently.

Thinking it through: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.

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

This prevents flapping when two nodes alternate between nearly identical lag results. A switch it makes sense to 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.

First, a clarification:If a connection log shows AI or Streaming, switching the generic PROXY group will not repair the request. This is one of the most of of 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.; Operate 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..

Mechanically: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 re-sync.

Scheduling, Observability, and Production Troubleshooting in Depth

On Linux or macOS, a timer, cron job, or system service can operate the health-check script. On Windows, Task Scheduler can kick off a PowerShell script at login or at a fixed interval. A five-minute interval is more often than not a better starting point than a thirty-second loop.

Make the script idempotent. Running it twice with the same healthy result it makes sense to 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, lag values, and exception categories; omit bearer tokens and subscription URLs.

More strictly:When a request returns an exception, classify it before changing the configuration. A connection refusal more often than not 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.

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.

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.

At a different level: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 by itself 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.

Thinking it through: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, obvious logs, and a fallback that leaves the last known-good node in place.

Compared with GUI-only clients, trim node-switching scripts offer repeatability but can become fragile when endpoint names, sign-in rules, and core versions differ; browser extensions and provider dashboards often lack it holds up 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 reasonably than guessing at ports and endpoints.

First, a clarification: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.