A minimal mihomo front-end. Three jobs: keep one subscription up to date, run the core, flip the system proxy. Everything a user would touch once a year stays in mihomo's config.
A Windows port of Yami, rewritten in C# — same product, same non-goals, same dozen failure modes the macOS version already paid for.
┌───────────────────────────────────────┐
│ CONNECTION │
│ Mihomo [ ●──] │
│ ● Running · port 7890 │
│ System proxy [ ●──] │
│ Routing [ Loyalsoldier ▾ ] │
│ │
│ SUBSCRIPTION │
│ ┌─────────────────────────────────┐ │
│ │ https://example.com/sub… ↻ │ │
│ └─────────────────────────────────┘ │
│ Updated 2 hours ago │
│ │
│ APP │
│ Start with Windows [ ●──] │
│ View config │
│ Open log folder │
│ Exit Yami │
│ Yami 0.3.0 · mihomo 1.19.30 │
└───────────────────────────────────────┘
Not to install, not to run, not to proxy.
Changing the system proxy on Windows is a per-user, unelevated operation — a WinINET call and
a few values under HKCU — so the entire privileged-helper apparatus the macOS version needs
(a root daemon, XPC, mutual code-signing requirements, a hardcoded team ID) collapses into one
class. The installer is per-user into %LOCALAPPDATA%\Programs\Yami. Nothing in this app ever
raises a UAC prompt.
This is the strongest thing about the port, and it is why TUN mode stays a non-goal: mihomo's
TUN mode needs wintun.dll and administrator rights, which would mean a service, an elevated
broker, or a UAC prompt per launch.
The non-goals are the product. No node list, no rule editor, no traffic graph, no multiple profiles, no port setting, no themes, no TUN mode, no LAN sharing.
Yami writes the WinINET proxy settings, which reach Edge, Chrome, most Chromium apps, .NET's default handler, and anything else that asks Windows for proxy configuration.
They do not reach apps that open raw sockets or ship their own network stack: Firefox by
default, many games, many Electron apps with explicit proxy settings, most CLI tools. This is
a real limitation and it is narrower than what macOS's SCPreferences gets you. It is stated
here rather than left for you to discover.
Four defences run, in order of coverage: a graceful exit turns the proxy off before stopping
the core; SessionEnding and ProcessExit cover sign-out, shutdown and most crashes; a
sentinel file makes the next launch notice a dirty shutdown and undo it; and
RegisterApplicationRestart asks Windows to relaunch Yami after a crash so that happens in
seconds rather than at your next sign-in.
The residual gap is real: a hard kill followed by never relaunching leaves the proxy pointed at a core that is gone, and every WinINET-aware app loses its network. The fix is to relaunch Yami, or turn the proxy off in Settings ▸ Network & internet ▸ Proxy.
There is deliberately no watchdog process. It would reintroduce most of the complexity that
deleting the privileged helper bought, to close a window RegisterApplicationRestart already
covers.
| Subscription | The provider's rules and rule-providers, untouched |
| Loyalsoldier | clash-rules — proxy by default, mainland China and LAN direct, ads rejected |
| Global | A single MATCH sending everything through the provider's group |
Loyalsoldier targets a group literally named PROXY, which no subscription is obliged to
define, so Yami substitutes whatever group the provider's own MATCH rule points at.
"Global" is not mihomo's mode: global — that routes through the GLOBAL selector, whose
selection defaults to DIRECT and does not persist without store-selected.
Changing routing re-renders from the cached subscription on disk, so it costs no round-trip and works offline.
The provider's YAML is trusted for proxies, proxy-groups and rules. Everything that
decides how Yami talks to the core is overridden:
mixed-port: 7890
allow-lan: false
mode: rule
log-level: warning
external-controller: ""
external-controller-pipe: \\.\pipe\dev.yami.<per-user>
tun:
enable: falseAnd these are removed outright, because a provider that sets them opens listeners you were
never told about: port, socks-port, redir-port, tproxy-port,
external-controller-tls, external-controller-unix, secret, external-ui.
Every rendered config is validated with mihomo -t against a staging file before it is
allowed to replace the one the core loads. A bad subscription can never take a working core
down.
Download the installer from Releases and run it. No UAC prompt.
SmartScreen will warn you — "Windows protected your PC". SmartScreen works on accumulated reputation rather than a check, and these builds are unsigned, so it warns harder than usual and names an unknown publisher. There is no way to pre-clear this. Choose More info ▸ Run anyway, and only from this repository's releases.
Antivirus false positives happen. A process that spawns a network daemon and rewrites proxy settings matches several heuristics, and an unsigned one matches them harder. If a scanner objects, the detection gets submitted to the vendor. See On signing for why nothing is signed yet and what would change that.
On a first run there is no config, so the core cannot start, so the proxy cannot be on, so the subscription fetch must go direct. If your provider is unreachable from that network without a proxy, there is no path from inside the app. This is a known, unsolved gap — the same one the macOS version has.
%LOCALAPPDATA%\Programs\Yami\ the app and the bundled core
%LOCALAPPDATA%\Yami\
config.yaml generated — subscription + Yami's overrides
subscription.yaml the provider's YAML as downloaded
settings.json url, lastUpdated, routing, saved PAC url
proxy.sentinel present while the proxy is believed on
cache.db, geoip.metadb, ruleset\ mihomo's own
logs\core.log
The subscription URL is stored in the clear. It embeds a token, so encrypting it is tempting —
but config.yaml next to it holds every node's password in plaintext, so protecting the URL
alone would buy nothing real.
config.yaml is generated and read-only. View config shows exactly what the core loaded;
edits made to the file are lost on the next update.
Needs the .NET 10 SDK. Windows only.
pwsh scripts/fetch-mihomo.ps1 -PinDownloads the pinned mihomo release into vendor\. Upstream publishes no checksums and no
signatures, so the first pin is trust-on-first-use: -Pin records the artefact's SHA-256
into scripts/mihomo.sha256, and every run after that verifies against it. Commit that file.
pwsh scripts/build.ps1 -InstallerRuns the tests, publishes self-contained into dist\, copies the core and its licence in,
and builds the Inno Setup installer. The version comes from git describe, so it cannot
drift from the tag.
dotnet test --project tests/Yami.Core.Tests/Yami.Core.Tests.csprojYami.Core has no UI reference at all, which is what makes config rendering, routing, the
subscription store, log condensing and the refresh policy testable without a UI thread.
dotnet test --project tests/Yami.Integration.Tests/Yami.Integration.Tests.csprojThe integration suite needs the real machine: it starts a real core, runs everything Yami
renders past mihomo -t, and exercises the port guard and the job object. It skips rather
than fails when vendor\mihomo.exe is missing or port 7890 is busy.
The system-proxy tests inside it are opt-in, because they write to
HKCU\...\Internet Settings and briefly point this PC at 127.0.0.1:7890. A fixture saves
every value they touch and restores it, including deleting values that did not exist before,
so a failing test cannot leave your machine proxied:
YAMI_ALLOW_PROXY_TESTS=1 dotnet test --project tests/Yami.Integration.Tests/Yami.Integration.Tests.csproj.github/workflows/ci.yml builds and tests every push and pull request. On main it
publishes a rolling prerelease tagged canary, replaced by each new commit, with the
installer renamed to a stable filename so its download URL never changes. Pushing a v*
tag publishes a normal immutable release with a changelog since the previous v* tag.
Builds are currently unsigned, and the workflow publishes them anyway rather than shipping nothing. Each release says so in its own notes: an unsigned installer draws a harder SmartScreen warning and is likelier to trip antivirus heuristics, which is worth knowing before you run it and worth stating plainly rather than burying.
Signing is wired up but switched off, because it costs real money for a project with no
install base yet — see the note below. Supply these repository secrets and the workflow
signs Yami.exe, mihomo.exe and the installer, in that order, so the installer packages
already-signed binaries:
AZURE_CLIENT_ID
AZURE_TENANT_ID # presence of this one is what turns signing on
AZURE_SUBSCRIPTION_ID
AZURE_SIGNING_ENDPOINT
AZURE_SIGNING_ACCOUNT_NAME
AZURE_SIGNING_CERT_PROFILE
Login is federated (OIDC), so no client secret is stored. The Entra application needs a federated credential matching this repository and the Trusted Signing Certificate Profile Signer role on the profile.
Azure Trusted Signing is roughly $120/year, which buys convenience rather than trust: signing does not skip the SmartScreen warning — only an EV certificate does that, and only sort of. What it genuinely reduces is antivirus false positives, which a tool that spawns a network daemon and rewrites proxy settings will eventually attract.
Cheaper routes exist and are worth preferring: SignPath Foundation provides free signing for open-source projects, and Certum's open-source certificate runs about $35/year. The plan is to sign when false positives actually show up, not before.
Yami is licensed under the GNU General Public License v3.0 — see LICENSE.
It bundles mihomo v1.19.30, which is also GPL-3.0. Yami runs the core as a separate
process rather than linking it, so the two are independent works, but bundling the binary is
redistribution either way: the core's licence ships next to the executable as
mihomo-LICENSE.txt and the exact tag is recorded in mihomo-VERSION.txt. Both are kept in
step with any version bump.
(Not legal advice — just the thing not to discover after shipping.)