YAML Structure Overview
Where the Config File Lives
GUI clients usually shield you from the raw file: Clash Verge Rev manages configs on its "Profiles" page, Clash Plus keeps subscriptions and local configs inside the app, and both store the actual files in their app data folders, with an "Open config folder" option somewhere in the UI. The mihomo core itself (and legacy clients like Clash for Windows) reads a single YAML file, named config.yaml by default, with the -d flag pointing to the folder it lives in. Whichever path you take, the file that ends up running has the same structure: a block of top-level scalar fields (port, mode, log level), followed by four functional sections — dns, proxies, proxy-groups, rules — plus two external-resource sections, proxy-providers and rule-providers. Once you understand this skeleton, you understand the config logic shared by every Clash-family client.
Four Hard Rules of YAML Syntax
YAML has very few syntax rules, but each one can make or break parsing:
- Indentation must use spaces only, never tabs. This is the number one cause of parse errors for newcomers — set your editor to convert tabs to two spaces and you'll never think about it again.
- A colon between key and value needs exactly one space after it. Writing
port:7890orport : 7890will either fail to parse or produce unexpected results. - List items start with "- ", also followed by a single space. Lists can be written at the same indent level as the parent key, or indented one level further — both are valid, just stay consistent throughout the file.
- If a string contains a colon, #, curly braces, square brackets, or commas, wrap the whole thing in double quotes. Anything after a # is a comment and gets ignored during parsing.
YAML also supports anchors and references (& defines an anchor, * references it, << merges it in), which lets you avoid repeating shared parameters across multiple nodes — see the "Proxy Node Fields" section for a concrete example.
Top-Level Fields at a Glance
A complete config's top level roughly splits into three groups: network listeners (port, socks-port, mixed-port, etc.), runtime behavior (mode, log-level, ipv6, external-controller, etc.), and functional sections (dns, proxies, proxy-groups, rules). The table below gives the big picture; each section is expanded further down.
| Field | Type | Purpose |
|---|---|---|
port | Integer | HTTP proxy listening port |
socks-port | Integer | SOCKS5 proxy listening port |
mixed-port | Integer | Combined HTTP + SOCKS port |
allow-lan | Boolean | Whether LAN devices can connect |
mode | Enum | Runtime mode: rule / global / direct |
log-level | Enum | silent / error / warning / info / debug |
external-controller | String | RESTful control API listen address |
dns | Section | Built-in DNS server and resolution behavior |
proxies | List | List of proxy nodes |
proxy-groups | List | List of proxy groups |
rules | List | Traffic rules, matched in order |
rule-providers | Map | External rule set definitions |
# config.yaml skeleton: scalars first, then sections
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
external-controller: 127.0.0.1:9090
dns:
enable: true
# ...see "DNS Fields" section for details
proxies:
- name: "node-a"
# ...see "Proxy Node Fields" section for details
proxy-groups:
- name: "PROXY"
type: select
proxies: ["node-a", DIRECT]
rules:
- DOMAIN-SUFFIX,local,DIRECT
- MATCH,PROXY
Quick Note
When YAML parsing fails, the core refuses to start; GUI clients usually show "config load failed" or an empty proxy list. The line number in the error is generally trustworthy — check the indentation, quotes, and colons around that line first.
General Fields: Ports, Mode & Global Behavior
Port Fields
Clash exposes four kinds of inbound listeners. port is the classic HTTP proxy port; socks-port is a SOCKS5 port that supports UDP forwarding; mixed-port combines both protocols on one port, with the core auto-detecting the request type — most GUI clients today default to only opening the mixed port. redir-port and tproxy-port are for Linux transparent proxying and aren't relevant on desktop. If startup fails with "bind: address already in use," that port is taken — switch ports, or find and stop whatever process is holding it. For the mixed port's real-world use in LAN sharing, see the deep dive in this technical note.
| Field | Protocol | Common Values | Notes |
|---|---|---|---|
port | HTTP | 7890 | Legacy client default, being phased out by mixed port |
socks-port | SOCKS5 | 7891 | Supports UDP forwarding, common for gaming/voice apps |
mixed-port | HTTP + SOCKS5 | 7890 / 7897 | Current mainstream default, one port for both protocols |
redir-port | Transparent proxy (redirect) | No default | Linux gateway/router setups |
tproxy-port | Transparent proxy (tproxy) | No default | Linux gateway setups, supports UDP |
Runtime Mode
mode controls how requests are routed, with three values: rule matches the rules list in order and is the default for everyday use; global sends all traffic through the GLOBAL proxy group (usually the node you've picked in the panel) — switching to it is the quickest way to check "is this a rule problem?"; direct sends everything unproxied, useful for confirming the proxy itself is working. The mode toggle on a GUI client's home screen changes exactly this field.
LAN & Remote Access
With allow-lan enabled, phones and tablets on the same network can point their proxy settings at this computer's IP and port and share the same outbound proxy; bind-address controls which network interface to listen on, defaulting to "*" for all interfaces — this only matters when paired with allow-lan. external-controller is the core's RESTful control API, which GUI panels use to read status, switch nodes, and pull logs; secret is the access token for that API. If you change this API's listen address to 0.0.0.0 to expose it externally, you must also set a secret — otherwise you're handing control of your proxy to the entire network. external-ui points to a static panel directory, letting you access a management UI directly from a browser.
Security Note
Only enable allow-lan on trusted networks. Opening a proxy port on public Wi-Fi means lending your bandwidth and exit node to strangers on the same network. For the full setup and verification steps for LAN sharing, see "Clash Mixed Port and LAN Proxy Sharing."
Logging, Latency & Other Behavior Fields
log-level has five tiers from silent to debug — bump it to debug temporarily when troubleshooting, and leave it at info the rest of the time. ipv6 controls whether AAAA records are handled; turn it off if your network has no IPv6 route. unified-delay makes latency tests include TCP/TLS handshake time, so numbers across nodes are actually comparable; tcp-concurrent lets candidate nodes connect in parallel for a faster first hop; find-process-mode controls whether local process names get resolved, which the PROCESS-NAME rule depends on; global-client-fingerprint sets a TLS client fingerprint to mimic (commonly chrome), relevant for protocols relying on uTLS; and store-selected under the profile section remembers your manual node picks in the panel so they survive a config reload. There's also a tun section for virtual-adapter-level traffic capture — an advanced topic that isn't needed for typical desktop use.
port: 7890
socks-port: 7891
mixed-port: 7897
allow-lan: false
bind-address: "*"
mode: rule
log-level: info
ipv6: false
external-controller: 127.0.0.1:9090
secret: ""
unified-delay: true
tcp-concurrent: true
find-process-mode: strict
global-client-fingerprint: chrome
profile:
store-selected: true
store-fake-ip: true
DNS Fields: Blocking Poisoning & Leaks
Why DNS Gets Its Own Section
The proxy only controls where traffic goes — DNS decides what a domain resolves to in the first place. By default, system DNS goes over plaintext UDP port 53 through your ISP: it can be hijacked, and it exposes what you're accessing to the local network, which is exactly what's meant by a DNS leak. Clash's dns section runs a built-in DNS server inside the core: it intercepts queries and routes different domains to different upstreams. Combined with fake-ip mode, it can even match rules by domain name first and decide whether to proxy afterward, sidestepping the chicken-and-egg problem of "the IP is already poisoned before the domain-based rule ever gets a chance to run."
Basic Fields
enable is the master switch; listen sets the built-in DNS server's listen address, which you can skip if the GUI client isn't doing system-level DNS takeover. enhanced-mode has two values: redir-host returns real resolution results, with the core tracking the domain-to-IP mapping itself; fake-ip returns a fake address in the 198.18.0.0/16 range immediately, then reverse-looks-up the domain once a connection actually comes in to match rules — it's faster and immune to poisoning, and is the mainstream choice today. fake-ip-filter lists domains that skip fake-ip — LAN device names, NTP time sync, and other services that need a real IP get resolved normally. default-nameserver only resolves the domain names of your upstream DNS servers themselves, so it must be plain-IP traditional UDP servers, or you end up needing DoH to resolve the DoH server's own domain — a circular dependency.
Upstream Servers
nameserver is your main upstream list; you can list several and the fastest response wins. fallback is a backup group used with fallback-filter based on GEOIP: if the resolved IP falls in a range outside your home region, it re-queries using fallback instead. mihomo recommends nameserver-policy instead: assign upstreams directly by domain-set, e.g. route geosite:cn through domestic DoH and everything else through a trusted DoH server outside — a more deterministic approach than fallback's query-then-check logic. proxy-server-nameserver is dedicated to resolving your proxy nodes' own domains, preventing a poisoned node domain from making the server unreachable. Turning on respect-rules makes DNS queries themselves follow your routing rules (e.g. querying a foreign DNS server through the proxy), at the cost of more configuration complexity. There's also a sniffer section that extracts domain names from TLS/HTTP handshakes, fixing rule matching for apps that connect directly by IP.
| Syntax | Protocol | Notes |
|---|---|---|
223.5.5.5 | UDP 53 | Traditional plaintext queries, only suitable for default-nameserver |
tls://dns.alidns.com | DNS over TLS | Encrypted transport over port 853 |
https://doh.pub/dns-query | DNS over HTTPS | Over HTTPS, hardest to interfere with |
quic://dns.example | DNS over QUIC | Supported by mihomo, low latency |
dhcp://en0 | DHCP-assigned | Uses the ISP DNS handed out by the network interface |
dns:
enable: true
listen: 0.0.0.0:1053
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "*.local"
- time.*.com
- ntp.*.com
default-nameserver:
- 223.5.5.5
- 119.29.29.29
nameserver:
- https://doh.pub/dns-query
- https://dns.alidns.com/dns-query
proxy-server-nameserver:
- https://doh.pub/dns-query
nameserver-policy:
"geosite:geolocation-!cn":
- https://dns.cloudflare.com/dns-query
- https://dns.google/dns-query
Troubleshooting Path
The classic symptom of a DNS leak: your browser is behind the proxy, but a DNS leak test site still shows your local ISP. Check in this order: dns.enable is on, your system or browser's DNS points at the core's listen address, and nameserver doesn't contain any ISP UDP servers. More symptom-to-fix mappings are on the FAQ page.
Proxy Node Fields: the proxies List
Common Fields
proxies is a list, with each entry describing one node. Only five fields are shared across every protocol: name (the display name, referenced by rules and proxy groups — must be unique across the whole file), type (protocol), server, port, and udp (whether to forward UDP — turn it on if the node supports it). Every other field depends on the protocol. When maintaining multiple nodes of the same protocol by hand, you can pull shared settings into a YAML anchor and have later nodes reference it with a merge key, writing only the fields that differ:
proxies:
- &ss-base
name: "ss-a"
type: ss
server: 203.0.113.10
port: 8388
cipher: aes-128-gcm
password: "your-password"
udp: true
- <<: *ss-base
name: "ss-b"
server: 203.0.113.11
port: 8389
Examples per Protocol
The examples below cover the five protocols most commonly used in mihomo. All addresses use documentation-reserved ranges — swap in your real server details to use them:
proxies:
- name: "ss-example"
type: ss
server: 203.0.113.10
port: 8388
cipher: aes-128-gcm
password: "your-password"
udp: true
- name: "vmess-example"
type: vmess
server: 198.51.100.20
port: 443
uuid: 11111111-2222-3333-4444-555555555555
alterId: 0
cipher: auto
tls: true
servername: example.com
network: ws
ws-opts:
path: /ray
headers:
Host: example.com
- name: "trojan-example"
type: trojan
server: 192.0.2.30
port: 443
password: "your-password"
sni: example.com
skip-cert-verify: false
udp: true
- name: "hy2-example"
type: hysteria2
server: 203.0.113.40
port: 443
password: "your-password"
sni: example.com
skip-cert-verify: false
- name: "vless-example"
type: vless
server: 198.51.100.50
port: 443
uuid: 11111111-2222-3333-4444-555555555555
network: tcp
tls: true
udp: true
flow: xtls-rprx-vision
servername: www.microsoft.com
reality-opts:
public-key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
short-id: "0123456789abcdef"
client-fingerprint: chrome
Only turn on skip-cert-verify temporarily if you have a specific, confirmed reason to bypass certificate chain validation — leaving it on long-term means giving up TLS identity verification entirely. sni / servername sets the domain declared during the TLS handshake; get it wrong and the connection simply won't go through.
Protocol Support Differences
Legacy Clash is no longer maintained, and new protocols only land in the mihomo core. That's also why every download recommendation on this site leads with mihomo-based clients:
| Protocol | Legacy Clash (discontinued) | mihomo |
|---|---|---|
| Shadowsocks | Supported | Supported |
| VMess | Supported | Supported |
| Trojan | Supported | Supported |
| Snell | Supported | Supported |
| Hysteria / Hysteria2 | Not supported | Supported |
| TUIC | Not supported | Supported |
| VLESS / Reality | Not supported | Supported |
| WireGuard | Not supported | Supported |
Note for Subscription Users
After importing a subscription, the client auto-generates the proxies list, so you rarely need to write this section by hand. Editing nodes directly inside a subscription file gets wiped the next time it updates — if you want to keep custom nodes long-term, use the append method described in the "Override & Merge" section instead.
Proxy Group Fields: proxy-groups
Five Types
A proxy group is a container of nodes paired with a selection strategy. select is manual selection — click one in the panel and traffic follows it; almost every config has at least one main entry group like this. url-test tests latency across all nodes in the group at a fixed interval and auto-picks the lowest one. fallback also tests latency, but its logic is "only switch when the current node goes down" — list order is priority order. load-balance spreads connections across multiple nodes (strategy is either consistent-hashing or round-robin), good for stacking bandwidth across multiple lines. relay chains several nodes together so traffic passes through each in sequence, exiting from the last one. mihomo also lets you add an icon field to a group for a custom icon in the panel.
| Type | Selection Method | Typical Use |
|---|---|---|
select | Manual selection | Main entry point, feature toggles (like an ad-block switch) |
url-test | Auto-pick lowest latency | Best node among several in the same region |
fallback | Failover | Primary/backup line, stability first |
load-balance | Load balancing | Stacking bandwidth across multiple lines |
relay | Chained relay | Multi-hop paths that separate entry from exit node |
Latency Test Parameters
url is the test target, conventionally http://www.gstatic.com/generate_204 since a 204 no-content response costs almost nothing; interval is the test interval in seconds — too short just wastes bandwidth and battery, 300 is common; tolerance is the switch threshold in milliseconds — a new node has to beat the current one by at least this much before switching, which avoids flip-flopping between two similarly fast lines; setting lazy to true only tests the group while it's actually in use, so idle backup groups don't burn traffic for nothing; timeout is the timeout for a single latency test.
Nesting & References
A proxy group's proxies list can reference the names of other proxy groups — this is nesting. A top-level PROXY group might contain three region groups — "Hong Kong," "Japan," "US" — each of which is a url-test group; you manually pick the region, and each region group auto-picks the fastest node inside it, with each layer doing its own job. The use field pulls in a whole batch of nodes from a proxy-providers subscription source; filter uses a regex to keep only matching node names, e.g. filter: "HK|Hong Kong" keeps only nodes with Hong Kong in the name. DIRECT and REJECT are two built-in exits that can be dropped straight into any proxies list, and combined with a select group they make handy feature toggles like an ad-block switch.
proxy-groups:
- name: "PROXY"
type: select
proxies: ["HK-AUTO", "JP-AUTO", "US-AUTO", DIRECT]
- name: "HK-AUTO"
type: url-test
use: ["provider-main"]
filter: "HK|Hong Kong"
url: http://www.gstatic.com/generate_204
interval: 300
tolerance: 50
lazy: true
- name: "JP-AUTO"
type: url-test
use: ["provider-main"]
filter: "JP|Japan"
url: http://www.gstatic.com/generate_204
interval: 300
- name: "US-AUTO"
type: fallback
use: ["provider-main"]
filter: "US|United States"
url: http://www.gstatic.com/generate_204
interval: 300
- name: "AD-BLOCK"
type: select
proxies: [REJECT, DIRECT]
About relay
If any single hop in a relay chain drops, the whole chain stops working, and latency stacks up with every extra hop. It solves a specific need — keeping entry and exit separate — rather than being a general speed trick; the first four group types cover ordinary use cases just fine.
Rule Syntax: rules from Top to Bottom
Match Order
rules is an ordered list: the core checks entries top to bottom and stops at the first match, ignoring everything below it. The hard rule of thumb is: specific rules go first, broad rules go later, and MATCH always sits at the very bottom. Rules split into two categories: domain-based rules (DOMAIN / DOMAIN-SUFFIX / DOMAIN-KEYWORD / GEOSITE) can be evaluated before resolution happens; IP-based rules (IP-CIDR / GEOIP) need a resolved address first. Adding no-resolve to an IP-based rule means "if the request is already a plain domain, don't trigger a DNS lookup just to check this rule" — pure domain requests skip that rule and keep going, avoiding a lot of pointless lookups. GEOSITE and GEOIP rely on geo data files; GUI clients keep these updated automatically, but if you're running the core bare, make sure the data files are in the config directory or that auto-update is enabled.
Rule Type Quick Reference
| Syntax | Matches | Notes |
|---|---|---|
DOMAIN,example.com,outbound | A single domain | Exact match, no subdomains |
DOMAIN-SUFFIX,example.com,outbound | Domain suffix | Matches the domain and all subdomains |
DOMAIN-KEYWORD,google,outbound | Domain keyword | Matches on substring — use with care |
GEOSITE,cn,outbound | Domain category list | Community-maintained category data |
IP-CIDR,192.168.0.0/16,outbound | IPv4 range | Add no-resolve when possible |
IP-CIDR6,fe80::/10,outbound | IPv6 range | Same as above |
GEOIP,CN,outbound | IP geolocation | Add no-resolve when possible |
SRC-IP-CIDR,192.168.1.201/32,outbound | Source IP | Per-device routing on your LAN |
DST-PORT,443,outbound | Destination port | Route by destination port |
PROCESS-NAME,telegram.exe,outbound | Process name | Requires find-process-mode |
RULE-SET,name,outbound | External rule set | References a rule-providers definition |
MATCH,outbound | Catch-all | Must be the last rule |
Rule Providers
Rule providers pull hundreds or thousands of rules out into an external file, leaving just one reference line in the main config. When type is http, it periodically fetches from url (every interval seconds) and caches it to path; when it's file, it reads a local file instead. behavior has three values: domain (a pure set of domain suffixes, loaded as a domain tree for the fastest matching), ipcidr (pure IP ranges), and classical (classic rule lines that can mix any rule type). The rule set file itself can be YAML (a payload list) or plain text with one rule per line.
rule-providers:
reject-ads:
type: http
behavior: domain
url: "https://example.org/rules/ads.yaml"
path: ./ruleset/ads.yaml
interval: 86400
local-direct:
type: file
behavior: classical
path: ./ruleset/direct.yaml
rules:
- RULE-SET,reject-ads,AD-BLOCK
- RULE-SET,local-direct,DIRECT
- DOMAIN-SUFFIX,lan,DIRECT
- GEOSITE,private,DIRECT
- GEOSITE,cn,DIRECT
- GEOIP,CN,DIRECT,no-resolve
- GEOSITE,geolocation-!cn,PROXY
- MATCH,PROXY
Always Add a Catch-All
Without a MATCH rule at the end, unmatched requests fall back to default behavior (which varies by core and version), which shows up as "some sites just won't load for no reason." Every config should end with a MATCH rule, usually pointing at your main entry proxy group.
Override & Merge: Subscriptions and Custom Config Together
Why You Shouldn't Edit the Subscription File Directly
A subscription is essentially a full config file pushed down by the provider, and the client re-fetches and overwrites it wholesale on a schedule. Any custom rules you add or proxy group edits you make directly in the subscription file vanish on the next update. The right approach is to split "subscription content" and "local customization" into two layers: the subscription handles nodes, your overrides handle your rules and fields, and the client merges the two into a final config at runtime. That way the subscription can update freely while your local layer stays untouched.
Client Override Mechanisms
Take Clash Verge Rev as an example: right-click a subscription entry to edit it, and you can maintain a global extended config (Merge) that applies across all subscriptions. Merge supports two kinds of keys: append keys with a prepend- / append- prefix, which insert content at the start or end of the matching list; and override keys sharing a name with a top-level field, which replace the subscription's value outright. Clash Plus offers a similar custom-config entry point outside the subscription itself. If you're still on Clash for Windows: that project is no longer maintained and its Mixin mechanism hasn't been updated in years — see "Migrating to Clash Verge Rev and Clash Plus" for a path to an actively maintained client.
# Global extended config (Merge) example
prepend-rules:
- DOMAIN-SUFFIX,internal.example.com,DIRECT
append-proxy-groups:
- name: "MY-SELECT"
type: select
use: ["provider-main"]
mixed-port: 7897
Three Approaches Compared
| Approach | Characteristics | Best For |
|---|---|---|
| Client override (Merge / custom config) | No external dependencies, travels with the client | Everyday rule tweaks, port changes, extra proxy groups — recommended |
| Local template + subscription conversion | Generates a complete config in one pass | Merging multiple subscriptions, fine-grained template management |
| Self-hosted subconverter | Full control, shareable across multiple users | Users with their own server, advanced setups |
Recommended Approach
Most users only need the client's built-in override, which covers the vast majority of customization needs — adding rules, changing ports, appending proxy groups. For subscription format differences and conversion methods, see the technical note "What Format Are Clash Subscription Links?"; grab the client itself from the download page, where Clash Plus is the top pick across all platforms.
Validation, Debugging & Common Errors
Static Checking with the Core
mihomo has a built-in config check: put config.yaml in a folder and run ./mihomo -t -d ./folder — it prints a success message if the check passes, or the field name and line number if it fails. For big changes, run -t from the command line first, then let the GUI load it — much faster than repeatedly restarting the client to find out what broke. Windows users can put the core executable and config in the same folder and run the same command from PowerShell.
Hot Reload & Logs
With external-controller enabled, you can reload the config without restarting the core:
curl -X PUT "http://127.0.0.1:9090/configs?force=true" \
-H "Content-Type: application/json" \
-d '{"path": "/absolute/path/config.yaml"}'
force=true forcibly disconnects and reconnects existing connections — you can usually skip it for a routine reload. On the logging side, bumping log-level to debug prints the matching process for every connection (domain, matched rule, exit node), which is your first tool for figuring out "why didn't this traffic go through the proxy?" A GUI client's logs / connections panel is essentially a visual layer over this same data.
Common Errors at a Glance
| Symptom / Error | Likely Cause | Fix |
|---|---|---|
| yaml: line N: did not find expected key | Inconsistent indentation or tabs mixed in | Standardize on 2-space indentation |
| cannot unmarshal | Wrong field type (e.g. port written as a string) | Check the type against the tables on this page |
| Config loads fine but every node times out | Subscription is outdated, or node domains are poisoned | Update the subscription; set proxy-server-nameserver |
| Some sites won't load | No rule matched and there's no MATCH catch-all | Add a MATCH rule at the end |
| Something breaks under fake-ip | That domain needs a real, resolved IP | Add it to fake-ip-filter |
| Panel can't connect to the core | Wrong external-controller address or secret | Double-check the listen port and token |
Don't Paste Your Config Into Online Tools
When troubleshooting, don't paste your whole config into an online YAML validator — it contains server addresses, ports, and passwords. A local editor's YAML plugin (VS Code, for instance) is enough for syntax checking, and the core's -t flag is enough for semantic checking.
That covers the full path from skeleton to fields, from rules to overrides. For more specific everyday issues — can't connect, slow speeds, an app not going through the proxy — the FAQ page organizes fixes by symptom; if you haven't installed a client yet, the download page lists actively maintained options by platform; and if you'd rather get the minimum setup running first and look up fields later, start with the setup guide.