Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save daviddwlee84/9fafacd99449b2e9483b081177166884 to your computer and use it in GitHub Desktop.

Select an option

Save daviddwlee84/9fafacd99449b2e9483b081177166884 to your computer and use it in GitHub Desktop.
macOS: Clash for Windows → Mihomo / Clash Verge Rev (VLESS Reality) — sanitized runbook

macOS + Ubuntu: Clash for Windows → Mihomo / Clash Verge Rev (VLESS Reality)

Sanitized runbook from a real migration on Intel macOS + LAN Ubuntu (2026-07).
Secrets (UUID / Reality keys / IPs that identify you) are redacted as <…>.


0. Why leave Clash for Windows?

Old stack Problem
Clash for Windows + Premium core 2023.05.29 Upstream Clash core is archived / deprecated
Same core No VLESS / Reality (needs Clash Meta → mihomo)

Working phone client (Shadowrocket) does not prove desktop CFW can use the same node.


1. Recommended clients (open source)

Client Role Notes
Clash Verge Rev GUI + embedded verge-mihomo Best daily driver on macOS
mihomo (Homebrew formula) Headless CLI core No GUI required; same YAML

You do not need the GUI if you are fine with:

mihomo -d ~/.config/mihomo          # reads ~/.config/mihomo/config.yaml
# mixed-port default in our profile: 7890
export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890

GUI is nicer for TUN / System Proxy / tray switching.

Other options (not used here): FlClashX, CLI-only mihomo + launchd, paid Stash.


2. Source node format → mihomo YAML

Input was a single Clash-Meta proxy snippet (type: vless + reality-opts + flow: xtls-rprx-vision).

That snippet alone is not a full profile. Wrap it:

mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
ipv6: true
external-controller: 127.0.0.1:9090

dns:
  enable: true
  enhanced-mode: fake-ip
  fake-ip-filter:
    - "*.lan"
    - "*.local"
    - "+.local"
  nameserver:
    - 1.1.1.1
    - 8.8.8.8

proxies:
  - name: Az Sg
    type: vless
    server: <SERVER_IP>
    port: 443
    uuid: <UUID>
    network: tcp
    udp: true
    tls: true
    flow: xtls-rprx-vision
    servername: support.apple.com          # Reality SNI / camouflage site
    reality-opts:
      public-key: <REALITY_PUBLIC_KEY>
      short-id: <REALITY_SHORT_ID>
    client-fingerprint: chrome

proxy-groups:
  - name: PROXY
    type: select
    proxies:
      - Az Sg
      - DIRECT

rules:
  # Critical under TUN: keep LAN / Tailscale / localhost DIRECT
  - IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
  - IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
  - IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
  - IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
  - IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
  - MATCH,PROXY

Shadowrocket (iOS) field map (for cross-check)

Shadowrocket YAML / meaning
Type VLESS type: vless
Address / Port server / port
Password / UUID uuid (not Shadowrocket’s internal uuid field)
Flow xtls: 2xtls-rprx-vision
Transport none network: tcp / obfs: none
TLS → Reality tls: true + reality-opts
SNI / peer servername / peer
Public Key / Short ID reality-opts.public-key / short-id
Fingerprint Chrome client-fingerprint: chrome

Share-link shape (also importable):

vless://<UUID>@<SERVER>:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=<SNI>&fp=chrome&pbk=<PUBLIC_KEY>&sid=<SHORT_ID>&type=tcp&headerType=none#Az%20Sg

3. Install paths we used

3a. mihomo CLI (fast — Homebrew bottle)

brew install mihomo
mkdir -p ~/.config/mihomo
cp ~/azp-mihomo.yaml ~/.config/mihomo/config.yaml
mihomo -t -d ~/.config/mihomo          # config test
mihomo -d ~/.config/mihomo             # run (foreground / nohup / brew services)

Smoke test:

curl -x http://127.0.0.1:7890 --http1.1 -s https://api.ipify.org
# expect egress IP == VLESS server (or its egress), not your home WAN

3b. Clash Verge Rev (GUI)

brew install --cask clash-verge-rev stalled on a slow GitHub asset (~63 MB).
Workaround that worked:

  1. Start mihomo CLI with the working VLESS profile (above).
  2. Download the cask DMG through that proxy:
export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890
curl -fL --http1.1 -o Clash.Verge_<ver>_x64.dmg \
  https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v<ver>/Clash.Verge_<ver>_x64.dmg
shasum -a 256 Clash.Verge_<ver>_x64.dmg   # must match brew cask sha256
  1. Install either by mounting the DMG → copy Clash Verge.app/Applications,
    or place the DMG in Homebrew’s download cache under the expected hash name and re-run brew install --cask clash-verge-rev.

First launch: install Service Mode helper (needed for TUN).


4. Programmatic Clash Verge Rev profile injection

App support dir (macOS):

~/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/
├── profiles.yaml          # index of profiles
├── profiles/
│   ├── Lxxxxx.yaml        # local profile body (Clash/mihomo YAML)
│   ├── Merge.yaml         # merge overlay companion
│   └── Script.js          # script companion
├── verge.yaml             # GUI settings (system proxy, TUN, ports…)
└── clash-verge.yaml       # runtime config generated for verge-mihomo

Schema notes (easy to get wrong)

  • Field name is type, not itype (older docs/code sometimes say itype).
  • Local profile UIDs conventionally look like L…; merge/script companions are often literally Merge / Script with files Merge.yaml / Script.js.
  • If you write profiles.yaml while the app is running, quit the app first — on startup it may rewrite the file.
  • After writing the profile body + index, restart the app (or use Profiles → refresh / Use).

Minimal profiles.yaml that stuck after restart:

# Profiles Config for Clash Verge

current: LazSg001
items:
- uid: LazSg001
  type: local
  name: Az Sg (VLESS Reality)
  file: LazSg001.yaml
  desc: Converted from azp.yaml
  updated: 1785044763
  option:
    merge: Merge
    script: Script
- uid: Merge
  type: merge
  name: null
  file: Merge.yaml
  updated: 1785044763
- uid: Script
  type: script
  name: null
  file: Script.js
  updated: 1785044763

Python sketch:

from pathlib import Path
import time

app = Path.home() / "Library/Application Support/io.github.clash-verge-rev.clash-verge-rev"
uid = "LazSg001"
body = Path("azp-mihomo.yaml").read_text()  # full profile YAML
(app / "profiles" / f"{uid}.yaml").write_text(body)
now = int(time.time())
(app / "profiles.yaml").write_text(f"""# Profiles Config for Clash Verge

current: {uid}
items:
- uid: {uid}
  type: local
  name: Az Sg (VLESS Reality)
  file: {uid}.yaml
  updated: {now}
  option:
    merge: Merge
    script: Script
- uid: Merge
  type: merge
  name: null
  file: Merge.yaml
  updated: {now}
- uid: Script
  type: script
  name: null
  file: Script.js
  updated: {now}
""")
# then: quit Clash Verge → reopen

Ports (this machine’s Verge defaults)

Setting Value
Verge mixed-port 7897 (see verge.yamlverge_mixed_port)
CLI mihomo profile 7890
Old Clash for Windows 7891 (~/.config/clash)

Do not run CFW + Verge + CLI mihomo on overlapping ports. Prefer one core.

UI checklist after import

  1. Profiles → select the local profile → Use
  2. Proxies → mode Rule or Global → select Az Sg (not DIRECT)
  3. Home → System Proxy on; TUN optional (needs helper)
  4. Confirm: curl -x http://127.0.0.1:7897 --http1.1 -s https://api.ipify.org

5. SSH to a LAN host (local_ubuntu) under TUN

~/.ssh/config pattern:

Host local_ubuntu
  HostName <LAN_IP>                 # e.g. 192.168.x.x
  User <user>
  Port 22
  IdentityFile ~/.ssh/<key>
  IdentitiesOnly yes
  ServerAliveInterval 20
  ServerAliveCountMax 6
  TCPKeepAlive yes

With TUN + MATCH,PROXY only, LAN SSH can hang or mis-route. Fix in the profile rules (section 2): IP-CIDR,192.168.0.0/16,DIRECT,no-resolve (+ Tailscale 100.64.0.0/10 if used).

Verify:

ssh -o ConnectTimeout=8 -o BatchMode=yes local_ubuntu 'hostname; whoami'

Optional: ProxyCommand=none if a global ProxyCommand exists.


6. Side note: copilot-proxy / Claude missing (egress geo-filter)

Not primarily org entitlement. GitHub serves Claude ids by egress geo:

Path Claude in /models
Direct / CN egress 0
Via overseas node (e.g. SG VLESS) 8+

Node / copilot-api / OpenCode ignore macOS System Proxy. CFW TUN/Mixin used to hide this by capturing all TCP. With System Proxy only, they cache a Claude-less catalog at process start.

Fix (chezmoi helpers):

proxy-status                 # Verge → 7897; Ubuntu mihomo → 7890
copilot-proxy restart        # COPILOT_HTTP_PROXY=auto → --proxy-env
copilot-proxy doctor         # A/B: direct 0 Claude vs via-proxy N Claude
copilot-model --auto         # Claude > Codex > GPT > Gemini from served list

OpenCode (if TUN healthy): no env needed. If TUN broken / System Proxy only:

HTTPS_PROXY=http://127.0.0.1:7897 opencode   # Mac Verge
HTTPS_PROXY=http://127.0.0.1:7890 opencode   # Ubuntu mihomo

7. Operational tips

  • Prefer Rule mode + LAN DIRECT rules; use Global only when debugging.
  • Never run CFW + Verge/mihomo TUN together — CFW owns 198.18.0.1 and plain HTTPS RSTs while -x :PORT still works.
  • Bootstrap chicken-and-egg: phone hotspot or any working proxy to fetch the first DMG/binary.
  • Core binary names: GUI uses verge-mihomo; CLI installs mihomo. Same Meta API.

8. Quick reference commands

# CLI client (Ubuntu / headless)
systemctl --user status mihomo
mihomo -t -d ~/.config/mihomo

# Egress (prefer plain once TUN works)
curl -fsS https://api.ipify.org; echo
curl -x http://127.0.0.1:7897 --http1.1 -s https://api.ipify.org; echo   # Mac Verge
curl -x http://127.0.0.1:7890 --http1.1 -s https://api.ipify.org; echo   # Ubuntu

# macOS system proxy (set by Verge)
scutil --proxy | egrep 'HTTPEnable|HTTPPort|HTTPSEnable|SOCKSEnable'

# Copilot
copilot-proxy doctor
copilot-model --auto

9. Ubuntu: headless mihomo + TUN (takeover from CFW)

Final working layout

Piece Value
Binary ~/.local/bin/mihomo
Config ~/.config/mihomo/config.yaml
Unit ~/.config/systemd/user/mihomo.service (no AmbientCapabilities — user systemd → exit 218)
Caps sudo setcap 'cap_net_admin,cap_net_bind_service=+ep' ~/.local/bin/mihomo
Mixed port 7890 (after CFW stopped)
Controller 127.0.0.1:9090
TUN enable: true, stack: system → iface Meta (198.18.0.1/30)
Outbound NIC interface-name: eno1
PROXY / Final Final → PROXY → Han Lee Az Sg (not DIRECT)

One-shot after install:

mihomo-takeover-tun   # stops clash-core-service, setcap, port→7890, restart
# Do NOT run bare `mihomo` while the user unit is active (port bind errors)

Verify:

ip -br addr | grep Meta
curl -fsS https://api.ipify.org; echo          # should be SG without env
curl -fsS -x http://127.0.0.1:7890 https://api.ipify.org; echo
systemctl --user status mihomo

Do you need Clash Verge on Ubuntu?

Optional. Headless mihomo + TUN already covers Node/OpenCode/git.
Clash Verge Rev ships Linux AppImage/deb if you want a tray GUI — same core family. Prefer one stack (Verge or systemd mihomo), not both.


10. Chezmoi helpers: tv clash / tv clash-api control mihomo

mihomo speaks the Clash Meta external-controller HTTP API. Existing Television channels already target that:

Channel Role
tv clash Browse local YAML (proxies / groups / rules)
tv clash-api Live /proxies, groups, rules, connections; Alt+S switch group
# On Ubuntu (controller auto-discovers 127.0.0.1:9090)
tv clash-api

# From Mac → Ubuntu controller (SSH tunnel; Ubuntu binds controller to localhost)
ssh -N -L 19090:127.0.0.1:9090 local_ubuntu &
CLASH_CONTROLLER=127.0.0.1:19090 tv clash-api

# Explicit remote LAN only if you rebind controller (security trade-off):
# external-controller: 0.0.0.0:9090   # then:
# CLASH_CONTROLLER=192.168.x.x:9090 tv clash-api

Keybindings (see chezmoi docs/tools/tv.md): Alt+S switch proxy into a group, Alt+T latency, Alt+R reload config, Alt+D open dashboard.

macOS Clash Verge caveat

Verge often exposes the API on a unix socket only (/tmp/verge/verge-mihomo.sock), not 127.0.0.1:9097. tv clash-api expects HTTP today.

Workarounds:

  1. Verge Settings → enable External Controller on an HTTP port, or
  2. Bridge: socat TCP-LISTEN:9097,reuseaddr,fork UNIX-CONNECT:/tmp/verge/verge-mihomo.sock then CLASH_CONTROLLER=127.0.0.1:9097 tv clash-api, or
  3. Drive the Ubuntu mihomo controller over SSH tunnel (above).

Shell loopback helpers (proxy-status, withproxy, …) already probe Verge 7897 / mihomo 7890 / 17890.


11. macOS vs Ubuntu cheatsheet (current)

macOS (Clash Verge Rev) Ubuntu (mihomo user systemd)
Core embedded verge-mihomo ~/.local/bin/mihomo
Config Verge profiles/*.yaml + overlay ~/.config/mihomo/config.yaml
Mixed port 7897 7890
Controller unix sock (or enable HTTP) 127.0.0.1:9090tv clash-api
System proxy Verge GUI optional; TUN covers most apps
TUN Verge TUN Mode, stack: system tun.enable + setcap (not AmbientCapabilities)
Quit CFW Required — remove Login Item sudo systemctl disable --now clash-core-service
Node / OpenCode TUN or HTTPS_PROXY=…:7897 TUN or HTTPS_PROXY=…:7890

12. Tailscale bypass (three layers) + Grafana check

Tailscale CGNAT (100.64.0.0/10, MagicDNS *.ts.net) needs protection at three layers. A mihomo DIRECT rule alone is not enough for browsers that first send traffic to macOS System Proxy (127.0.0.1:7897) — that path dials “DIRECT” inside the core and often times out / 502s.

Layer Where What to add
System Proxy bypass macOS ExceptionsList / Verge verge.yamlsystem_proxy_bypass 100.64.0.0/10, *.ts.net (keep use_default_bypass: true)
TUN route exclude tun.route-exclude-address 100.64.0.0/10, fd7a:115c:a1e0::/48
Rules (defense in depth) profile / clash-rules direct DOMAIN-SUFFIX,ts.net, IP-CIDR,100.64.0.0/10, IP-CIDR6,fd7a:115c:a1e0::/48,no-resolve

Do not put tailscale.com in DIRECT/bypass on GFW networks — control-plane may still need the overseas proxy. Only bypass tailnet addresses / *.ts.net.

Clash Verge Merge gotcha

Putting a rules: block in profiles/Merge.yaml can replace the whole profile rule list (left us with 3 rules → everything DIRECT). Keep Merge tun-only; put Tailscale DIRECT lines in the profile (or in public clash-rules).

Verified (2026-07-26)

Check Result
http://100.77.43.16:39000/ direct / plain HTTP 302/login (Grafana)
Browser → same URL Grafana login page
Route to 100.77.43.16 Tailscale iface (utun10), not mihomo TUN
macOS ExceptionsList includes 100.64.0.0/10, *.ts.net
Public egress via Verge / TUN 104.215.179.243 (SG) after rules restored
Ubuntu mihomo TUN exclude + rules applied; plain egress still SG

Ownership / future personal repo (roadmap only)

Repo / surface Owns
clash-rules Public routing policy (direct includes Tailscale IP/MagicDNS)
Per-device profile (Verge Merge / Ubuntu config.yaml) TUN excludes, System Proxy bypass, LAN DNS
DockerCompose-V2Ray Server / vault credentials (leave as-is for now)
Future personal repo Encrypted node providers, base mihomo profiles, soft-router align, one-click bootstrap

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment