---
name: clash-docker-workflow
description: Deploy and manage a high-performance proxy (Clash/Mihomo) — standalone binary (preferred) or Docker. Includes Web UI dashboards and Hermes Telegram proxy integration.
tags: [proxy, clash, mihomo, networking]
---
# Clash / Mihomo Proxy Workflow

Deploy and manage a high-performance proxy (Clash/Mihomo) for routing traffic through China's GFW.

Two deployment modes: **standalone binary** (preferred by user) or **Docker**.

---

## Mode 1: Standalone Binary (preferred)

### Install
```bash
# Download mihomo — use GitHub mirror in China (direct github.com times out)
cd ~/clash
curl -L "https://ghfast.top/https://github.com/MetaCubeX/mihomo/releases/download/v1.19.8/mihomo-linux-amd64-v1.19.8.gz" -o mihomo.gz
gunzip -f mihomo.gz && chmod +x mihomo
./mihomo -v  # verify
```

### Configure
```bash
# 1. Create config dir
mkdir -p ~/clash/config

# 2. Download subscription — MUST set User-Agent or WAF blocks (403)
curl -L -H "User-Agent: clash-verge/v2.0.0" \
  "SUBSCRIPTION_URL" -o ~/clash/config/config.yaml

# 3. Inject required params if missing
#    Raw subscriptions often omit these:
cat >> ~/clash/config/config.yaml << 'EOF'
mixed-port: 7890
allow-lan: true
mode: rule
log-level: info
external-controller: 0.0.0.0:9090
EOF
```

### Pre-download GeoIP data (critical for China servers)
mihomo downloads GeoIP/Geosite DBs from GitHub on first start. If GitHub is blocked, it **hangs forever** at "Can't find MMDB, start download". Pre-download via mirror:
```bash
cd ~/clash/config
curl -L "https://ghfast.top/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.db" -o geoip.db
curl -L "https://ghfast.top/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb" -o geoip.metadb
curl -L "https://ghfast.top/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.db" -o geosite.db
```

### Run
```bash
# Foreground (test, Ctrl+C to stop)
~/clash/mihomo -d ~/clash/config

# Background — use Hermes terminal background mode:
# terminal(background=true, command="cd ~/clash && exec ./mihomo -d config 2>&1")
# Then check: terminal(command="ss -tlnp | grep -E '7890|9090'")
```

**⚠️ `terminal(background=true)` is NOT persistent.** The mihomo process dies when:
- The gateway session ends or restarts
- The Hermes turn that started it finishes and gets garbage-collected
- The system reboots

For production use, **always use systemd** (see references/systemd-setup.md). Only use `terminal(background=true)` for quick tests.

### Verify
```bash
curl -x http://127.0.0.1:7890 https://api.telegram.org  # should return HTML/JSON (not timeout)
curl http://127.0.0.1:9090/version                        # {"meta":true,"version":"v1.19.8"}
```

---

## Mode 2: Docker

### docker-compose.yml
```yaml
services:
  clash:
    image: orzly/mihomo:latest
    container_name: clash
    restart: always
    volumes:
      - ./config:/root/.config/mihomo
    ports:
      - "7890:7890"
      - "7891:7891"
      - "9090:9090"
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
  clash-ui:
    image: yaknet/yacd:latest
    container_name: clash-ui
    restart: always
    ports:
      - "3412:80"
    environment:
      - API_URL=http://clash:9090
    depends_on:
      - clash
```

### Workflow
1. `mkdir -p ~/clash/config`
2. Place `docker-compose.yml` in `~/clash/`
3. Download subscription to `~/clash/config/config.yaml`
4. `cd ~/clash && docker compose up -d`
5. UI at `http://localhost:3412`

---

## Web UI (Dashboard)

### Option A: Online dashboard (zero setup, LAN only)
Use a hosted dashboard pointing to your mihomo API:
- **YACD**: https://yacd.haishan.me/ → API: `http://<server-ip>:9090`
- **metacubexd**: https://metacubex.github.io/metacubexd/ → API: `http://<server-ip>:9090`

**⚠️ Mixed content blocker**: Online dashboards are served over HTTPS. If your mihomo API is HTTP (typical for LAN), browsers block the fetch due to mixed-content policy. The dashboard loads but shows an empty/disconnected state. Fix: use Option B (local deployment).

### Option B: Local static deployment (recommended for LAN)
Download yacd static files and serve locally over HTTP:
```bash
cd ~/clash
curl -L "https://ghfast.top/https://github.com/haishanh/yacd/releases/download/v0.3.8/yacd.tar.xz" -o yacd.tar.xz
mkdir -p yacd && tar xf yacd.tar.xz -C yacd --strip-components=1
# Serve on port 9091 (avoids conflict with mihomo API on 9090)
python3 -m http.server 9091 --bind 0.0.0.0 --directory ~/clash/yacd &
```
Access with API pre-filled: `http://<server-ip>:9091/?host=http://<server-ip>:9090`

For persistence, add the python HTTP server to the same systemd service as mihomo (see references/systemd-setup.md).

### Option C: Docker container (see Mode 2 above)
YACD at `http://localhost:3412`.

---

### Connecting Applications

Set proxy env vars in `~/.bashrc` (Hermes terminal auto-sources it via `auto_source_bashrc: true`):
```bash
# Proxy for all HTTP/HTTPS traffic
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
export ALL_PROXY=socks5://127.0.0.1:7890

# NO_PROXY: local/internal services that must NOT go through proxy
# Include: localhost, local model servers, WireGuard subnet, .local mDNS
export NO_PROXY=localhost,127.0.0.1,192.168.199.100,10.8.0.0/24,*.local
```

**Important**: Set BOTH uppercase AND lowercase variants (`HTTP_PROXY` + `http_proxy`). Some libraries only read one form.

### Routing Specific Services via Mihomo Rules

When you need certain API services to **always** go through the proxy (even if MATCH rule changes), add explicit `DOMAIN-SUFFIX` rules to the top of the `rules:` section in `config.yaml`:

```yaml
rules:
    # LongPort / LongBridge API
    - 'DOMAIN-SUFFIX,longportapp.com,BiXin Network'
    - 'DOMAIN-SUFFIX,longport.com,BiXin Network'
    - 'DOMAIN-SUFFIX,longbridge.com,BiXin Network'
    - 'DOMAIN-SUFFIX,longbridge.hk,BiXin Network'
    # OKX 交易所
    - 'DOMAIN-SUFFIX,okx.com,BiXin Network'
    - 'DOMAIN-SUFFIX,okx.me,BiXin Network'
    - 'DOMAIN-SUFFIX,okx.co,BiXin Network'
    - 'DOMAIN-SUFFIX,oklink.com,BiXin Network'
    - 'DOMAIN-SUFFIX,oklink.org,BiXin Network'
    # ... existing rules below ...
```

Place service-specific rules **before** the large DIRECT rule block for Chinese services — Mihomo evaluates rules top-down, first match wins.

### Mihomo API Hot-Reload (no restart needed)

After editing `config.yaml`, reload without killing the process:
```bash
# Method 1: REST API reload
curl -s -X PUT http://127.0.0.1:9090/configs \
  -H 'Content-Type: application/json' \
  -d '{"path":"/home/openclaw/clash/config/config.yaml"}'

# Method 2: SIGHUP (simpler, same effect)
pkill -HUP -f mihomo

# Method 3: Full restart (only if hot-reload fails)
bash ~/clash/startup.sh restart mihomo
```

### Hermes Proxy Configuration

Hermes supports Telegram proxy via config.yaml:

```bash
hermes config set telegram.proxy_url http://127.0.0.1:7890
hermes gateway restart
```

`load_gateway_config()` bridges `telegram.proxy_url` to `TELEGRAM_PROXY` env var at startup. Verified working.

For full detail see: `skill_view(name="clash-docker-workflow", file_path="references/hermes-telegram-proxy.md")`/...`. Other mirrors: `ghproxy.com`, `mirror.ghproxy.com`.
- **Subscription URL returns 403 (WAF)**: Many subscription providers block requests without a browser-like User-Agent. Add `-H "User-Agent: clash-verge/v2.0.0"` to curl. This was the fix for BiXin (sub-route.bxy.re).
- **mihomo hangs at "Can't find MMDB, start download"**: GeoIP/Geosite DBs need to be pre-downloaded when GitHub is blocked. See "Pre-download GeoIP data" section above. Without these files, mihomo starts but never binds ports.
- **mihomo.gz corrupted on failed download**: If `file mihomo.gz` shows `ASCII text` instead of gzip, the download failed (GitHub returned 404/HTML). Re-download using mirror.
- **Missing Control Params**: A raw subscription file often lacks `external-controller` or `mode`. Always ensure these are present in `config.yaml` for the API/UI to work.
- **Container Communication (Docker only)**: The UI container uses `API_URL=http://clash:9090`. This relies on the service name `clash` in the Docker network.
- **Subscription config format**: mihomo (MetaCubeX) uses a slightly different config format than original Clash. Ensure your subscription provider supports mihomo/clash-meta format, or convert.
- **external-controller binding**: Default `127.0.0.1:9090` only allows local access. Use `0.0.0.0:9090` if you need remote Web UI access from another machine (e.g., Mac → Linux server).
- **Background process output lost**: When running mihomo with `terminal(background=true)`, stdout/stderr go to pipes. Use a wrapper script with `exec ./mihomo -d config 2>&1` or redirect to a log file to capture output.
- **Online dashboard shows empty/disconnected**: Browser blocks HTTP API requests from HTTPS dashboard pages (mixed-content policy). Deploy yacd locally instead — see Web UI Option B.
- **`{"hello":"mihomo"}` when opening API URL**: This is the mihomo API root response, NOT the Web UI. Users sometimes confuse this. The Web UI is a separate static app (yacd) that connects to the API.
- **Hot-reload preferred over restart**: Editing `config.yaml` then calling `PUT /configs` or `pkill -HUP` reloads rules without dropping active connections. Only do a full restart if hot-reload fails or the binary itself needs updating.
- **NO_PROXY must include internal IPs**: If you route all traffic through proxy, local services (Ollama on 192.168.x.x, WireGuard 10.8.0.x, localhost) will break. Always set NO_PROXY with internal subnets. Also include `*.local` for mDNS.
- **Lowercase proxy vars**: Some Python libraries (requests, httpx, aiohttp) read `http_proxy`/`https_proxy` (lowercase) while others read `HTTP_PROXY`/`HTTPS_PROXY` (uppercase). Set both to be safe.
- **Mihomo rule order matters**: Rules are evaluated top-down, first match wins. Place specific service rules (e.g., `DOMAIN-SUFFIX,okx.com`) BEFORE broad DIRECT rules (e.g., `DOMAIN-SUFFIX,qq.com`). If a broad DIRECT rule matches first, your specific proxy rule is never reached.
- **Proxy down = ALL platforms fail**: When `127.0.0.1:7890` is unreachable, Telegram, QQBot, and WeChat all fail simultaneously. Gateway log pattern: `Connect attempt N/8 failed: httpx.ConnectError: All connection attempts failed` + `Proxy detected; passing explicitly to HTTPXRequest`. Quick diagnosis: read `~/.hermes/logs/gateway.log` via `read_file()` (last 100 lines). For full diagnostic checklist: `skill_view(name="clash-docker-workflow", file_path="references/proxy-diagnostics.md")`.
- **mihomo systemd service**: The binary runs as a foreground process. For production, create a systemd service to survive reboots. Full recipe: `skill_view(name="clash-docker-workflow", file_path="references/systemd-setup.md")`.
- **Security scanner blocks sudo writes to /etc/systemd/**: Hermes's security layer intercepts `sudo tee`, `sudo cp`, and `write_file` targeting `/etc/systemd/system/`. Workaround: write `.service` files to `/tmp/` via `write_file()`, then instruct the user to manually `sudo cp` and `sudo systemctl`. Do not retry blocked commands — they will keep failing.
- **startup.sh vs systemd conflict**: If both `startup.sh` and systemd manage the same services, they can fight over PIDs. After enabling systemd, stop using `startup.sh` for start/stop — use `sudo systemctl` exclusively.
- **startup-service-manager skill**: For generic service management (not just Clash), see the `startup-service-manager` skill — same `~/startup/` directory, manages mihomo + yacd + any other service.
- **Subscription update MUST NOT use proxy (circular dependency)**: The proxy depends on the subscription for node configs. If subscription updates go through the proxy, you get a dead-lock: proxy needs subscription → subscription update needs proxy. **Always use direct connection (no `-x` flag) for subscription downloads.** This may be slower or fail on some networks, but it avoids the circular dependency. If direct download also fails (e.g., the subscription provider is blocked by ISP), the only escape is fetching from an alternate network (mobile hotspot, VPN) and manually placing the config file.
- **Duplicate mihomo processes**: If mihomo is started multiple times (e.g., both `startup.sh start` and a manual `./mihomo -d config`), duplicate PIDs compete for the same ports. Check with `ps aux | grep mihomo | grep -v grep` and kill extras. The `scripts/update-sub.sh` uses `flock` to prevent concurrent updates, but the mihomo binary itself has no singleton guard.
- **`hermes gateway restart` fails when run from inside the gateway**: Hermes blocks self-restart from within a gateway session (SIGTERM propagation). Instruct the user to run it from a separate shell. The agent cannot do this via terminal(). Workaround: `systemctl --user restart hermes-gateway` from a separate shell.
- **mihomo crash can permanently kill Telegram adapter (reconnect watcher bug)**: When mihomo goes down, the Telegram adapter hits 10 network retries → sets fatal error → gateway's `_platform_reconnect_watcher` is supposed to auto-reconnect with exponential backoff (30s→300s cap). BUT the watcher task can silently die (asyncio task exit with no log), leaving the adapter permanently offline even after mihomo recovers. Symptoms: (1) zero `[Telegram]` logs in journalctl after the crash, (2) no "Reconnecting telegram" or "Reconnect telegram error" entries, (3) DMs may still work (existing session) but group messages are lost. Diagnosis: `journalctl --user -u hermes-gateway --since '1 hour ago' | grep -i 'telegram' | grep -v Traceback`. If only crash/reconnect-failed logs exist with no subsequent "Reconnecting" attempts, the watcher is dead. Fix: restart gateway from external shell. This is a Hermes upstream bug — the watcher's `while self._running` loop exits silently under certain conditions. Until fixed upstream, consider a mihomo watchdog (see `references/mihomo-watchdog.md`) to prevent the initial crash.
- **Dual defense for proxy — set BOTH config bridge AND systemd env vars**: `telegram.proxy_url` in config.yaml IS bridged to `TELEGRAM_PROXY` by `load_gateway_config()`. But if the bridge fails (manual start, config parse error), the gateway falls back to direct connection. Always also set `HTTPS_PROXY` in the systemd service file for belt-and-suspenders reliability. Full diagnostic checklist: `skill_view(name="clash-docker-workflow", file_path="references/proxy-diagnostics.md")`.
- **channel_prompts are platform-agnostic in config.yaml**: The same group ID prompt configured under `telegram.channel_prompts`, `discord.channel_prompts`, and `slack.channel_prompts` is redundant — only configure it under platforms actually in use. Check which platforms are connected in `gateway_state.json` before auditing.
- **Old gateway services linger**: `~/.config/systemd/user/` may contain services from previous tools (e.g. `openclaw-gateway.service`). If the service is no longer needed, disable it: `systemctl --user disable --now <name>`. Otherwise it wastes resources on boot.
- **Proxy works but Telegram still times out**: Two fixes available — (1) `hermes config set telegram.proxy_url http://127.0.0.1:7890` then `hermes gateway restart` (simpler — `load_gateway_config()` bridges this to `TELEGRAM_PROXY` env var), OR (2) add `Environment="HTTPS_PROXY=http://127.0.0.1:7890"` to `~/.config/systemd/user/hermes-gateway.service`, then `systemctl --user daemon-reload && hermes gateway restart`. Symptom: curl through proxy passes, but gateway logs show connection failures. Full diagnostic checklist: `skill_view(name="clash-docker-workflow", file_path="references/proxy-diagnostics.md")`.

---

- **Mihomo watchdog to prevent cascading failures**: mihomo crashes can silently kill the Telegram adapter's reconnect watcher. A lightweight watchdog script monitors mihomo health and restarts it before downstream services notice. Full recipe: `skill_view(name="clash-docker-workflow", file_path="references/mihomo-watchdog.md")`.
- **⚠️ CRITICAL: systemd oneshot cgroup kills mihomo on watchdog exit**: If the watchdog runs as a systemd user service (`Type=oneshot`) and calls `startup.sh restart mihomo`, the mihomo process becomes a child of the watchdog's cgroup. When the watchdog service exits, **systemd kills all processes in the cgroup — including the just-started mihomo**. This creates a death loop: watchdog starts mihomo → watchdog exits → systemd kills mihomo → next watchdog cycle finds mihomo dead → repeat every 2 minutes. Symptoms: mihomo log shows `Mihomo shutting down` 3-6 seconds after each startup, watchdog.log shows repeated restarts. **Fix**: (1) mihomo MUST run as its own dedicated systemd service (not via startup.sh inside the watchdog), (2) watchdog calls `systemctl --user restart mihomo`, (3) watchdog must NOT check proxy connectivity — proxy node issues ≠ mihomo issues, unnecessary restarts break stable connections. See `references/mihomo-watchdog.md`.
- **Telegram adapter diagnostics**: When the bot stops processing group messages, use the diagnostic checklist in `references/telegram-adapter-diagnostics.md` — covers adapter health, proxy checks, bot membership, config verification, forwarder DB inspection, and the reconnect watcher architecture.

## Subscription URL Management\n\nHardcoding the subscription URL in multiple scripts creates a maintenance headache — change one place and the other drifts. Use a single-source-of-truth file:\n\n**Create `~/clash/sub-url.conf`:**\n```bash\n# Clash 订阅地址 — 改这一行，所有脚本自动读取\nSUB_URL=\"https://sub-route.bxy.re/api/v1/client/subscribe?token=YOUR_TOKEN_HERE\"\n```\n\n**Have all scripts source it:**\n```bash\n# In startup.sh, update-sub.sh, or any other script:\nif [ -f \"$CLASH_DIR/sub-url.conf\" ]; then\n    source \"$CLASH_DIR/sub-url.conf\"\nfi\n# Now $SUB_URL is available everywhere\n```\n\nThe `scripts/update-sub.sh` in this skill can also accept the URL as a CLI argument (`bash update-sub.sh \"URL\"`), which overrides the default for one-off tests.\n\n---\n\n## Subscription Auto-Update (Cron)

Proxy nodes rotate periodically. Set up a recurring cron job to keep the config fresh:

1. **Direct-connection update script**: `scripts/update-sub.sh` in this skill — downloads the subscription **directly (no proxy)**, injects required params (`external-controller`, `mixed-port`, etc.), and hot-reloads mihomo via `PUT /configs`. No restart needed unless hot-reload fails.

   **Why no proxy?** The proxy itself depends on the subscription for node configs. If the update script routes through the proxy, you get a circular dependency: proxy needs subscription → update needs proxy → deadlock. Always download subscriptions via direct connection.

2. **Cron job**: Run every 30 minutes. Only notify on failure:
   ```bash
   # Hermes cronjob approach:
   cronjob(action='create', schedule='30m', repeat=forever, name='Clash 订阅更新',
            prompt='bash ~/clash/update-sub.sh. Report only on failure.',
            enabled_toolsets=['terminal', 'file'])
   ```
