Clash External Controller API: Advanced Node Switching Guide

What the Clash External Controller Actually Controls Key Takeaways

The controller is serious because it exposes live state fairly 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 response time tests, a script can make a decision based on current reachability.

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.

Instead of yourself clicking through the proxy page, it is possible to query the active configuration, list available nodes, test response time, select a member inside a proxy group, inspect live connections, and build an automatic node-switching workflow around those operations.

The key point:This is different from changing a YAML profile on disk. The API operates on the running core and as a rule 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 are best off distinguish between a temporary switch and a persistent configuration change.

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. A fair few desktop clients expose the same setting in an advanced settings panel, while others place it inside the generated YAML.

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.

Let me put it this way: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.

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

Enable and Secure the Controller Before Automating It in Brief

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

Spin up 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 choice.

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

Verification is normally supplied through the secret configuration field.

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

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:

Where this lands: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 verification.

  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 fairly than reusing your subscription URL, password, or mixed-port credentials.
  3. Restart or reload the core: a fair few clients only apply controller changes after a core restart, while others reload the active configuration.
  4. Test verification: request /version and confirm that an invalid or missing token returns an authorization fault.
  5. Restrict the automation file: keep the token in an environment variable or a protected secret file, not straightforward in a script committed to Git.

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.

$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

Do not skip this: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 entire request headers when debugging CI logs.

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.

Inspect Proxy Groups and Test Nodes Safely A Closer Look

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

The the greater part of useful discovery endpoint is as a rule /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.

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

The key point:The exact response shape can vary between legacy Clash cores and newer Mihomo releases, so scripts are best off check for missing fields fairly than assuming every proxy is a selectable node.

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.

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 yourself concatenating unsafe characters.

Let me put it this way:Response time 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.

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"

A slim 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 are best off combine several signals. First, verify that the node exists in the current response. Second, measure response time with a timeout that reflects the use case. Third, reject HTTP faults, timeouts, and implausibly unstable results.

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 are best off 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.

Concretely:Fourth, apply a policy that prevents a node with slightly better response time from replacing a trustworthy node every few seconds. This last point matters because automatic switching can create more disruption than a slow but trustworthy connection.

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.

Switch a Proxy Group Programmatically What Matters

For a scheduled job, keep the workflow small and observable:

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.

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"

Where this lands: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 upgrade.

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

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

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

Do not skip this: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.

This prevents flapping when two nodes alternate between nearly identical response time results. A switch are best off 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.

The key point:If a connection log shows AI or Streaming, switching the generic PROXY group will not repair the request. This is one of the the greater part of common reasons an API automation script appears to work while application traffic remains unchanged.

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.; Work 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..

Scheduling, Observability, and Production Troubleshooting Key Takeaways

Compared with GUI-only clients, slim node-switching scripts offer repeatability but can become fragile when endpoint names, verification rules, and core versions differ; browser extensions and provider dashboards often lack trustworthy group control, while custom scripts tend to provide little guidance when a rule or DNS path is wrong.

On Linux or macOS, a timer, cron job, or system service can work the health-check script. On Windows, Task Scheduler can spin up a PowerShell script at login or at a fixed interval. A five-minute interval is as a rule a better starting point than a thirty-second loop.

Let me put it this way: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.

Make the script idempotent. Running it twice with the same healthy result are best off 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, response time values, and fault categories; omit bearer tokens and subscription URLs.

Concretely:When a request returns an fault, classify it before changing the configuration. A connection refusal as a rule 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.

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.

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.

Where this lands: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 without you 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.

Do not skip this: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, unambiguous logs, and a fallback that leaves the last known-good node in place.

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 fairly than guessing at ports and endpoints.