Yet Another Mihomo Interface — a minimal macOS menu bar app for mihomo.
Yami does three things: keeps one subscription up to date, runs the mihomo core, and flips the system proxy. Anything you would touch once a year lives in mihomo's own config, not in this UI.
┌─────────────────────────────────────┐
│ CONNECTION │
│ Mihomo [ ●──] │
│ ● Running · port 7890 │
│ System Proxy [ ●──] │
│ Routing [ Loyalsoldier ▾ ] │
│ │
│ SUBSCRIPTION │
│ ┌───────────────────────────────┐ │
│ │ https://example.com/sub… ↻ │ │
│ └───────────────────────────────┘ │
│ Updated 2 hours ago │
│ │
│ APP │
│ Launch at Login [ ●──] │
│ View Config │
│ Reveal Log │
│ Quit Yami │
│ Yami 0.3.0 · mihomo 1.19.30 │
└─────────────────────────────────────┘
The menu bar mark carries both states independently — the core can run without the proxy, so a single dimmed→solid ramp would conflate them:
Left to right: both off, core running, proxy on, both on. Dimmed means the core is stopped; the badge dot means the system proxy is on.
No node list or latency testing, no rule editor, no traffic graph, no multiple profiles, no TUN mode, no port configuration, no themes. The subscription's YAML decides proxies and rules; Yami never second-guesses it.
- macOS 14+ on Apple Silicon
- An Apple code signing identity — the privileged helper will not load unsigned
brew install mihomo, unless you bundle the core (below)
SwiftPM plus a bundling script; there is no .xcodeproj.
./build.sh # debug → build/Yami.app
./build.sh release # release
open build/Yami.appbuild.sh picks the first available signing identity; override with
YAMI_IDENTITY="Developer ID Application: …" ./build.sh release.
./build.sh release ships mihomo inside the app, so it runs on machines without
Homebrew. scripts/fetch-mihomo.sh downloads a pinned
arm64 release into a gitignored vendor/ and checks it against a SHA-256
recorded in the script; a debug build bundles it only if it is already there, and
otherwise falls back to /opt/homebrew/bin/mihomo at runtime. The binary adds
about 43 MB, and pins the core version to whatever the script says.
Upstream publishes no checksums or signatures for its release assets, so the
recorded hash comes from a download verified by hand. Everything after that first
pin is reproducible — a changed artefact fails the fetch rather than being signed
and shipped. The core is re-signed with your identity and the hardened runtime
before the bundle is sealed, since upstream ships it ad-hoc signed as a.out,
which will not notarize.
mihomo is GPL-3.0. Yami runs it as a separate process, so Yami itself is not
a derivative work, but bundling the binary is redistribution: the licence ships
in Contents/Resources/mihomo-LICENSE.txt, and the corresponding source is the
pinned tag at
MetaCubeX/mihomo.
Keep both in step if you bump the version. (Not legal advice — just the thing not
to discover after shipping.)
Building under a different Apple developer account requires changing one
thing: HelperInfo.clientRequirement and helperRequirement in
Sources/YamiShared/HelperProtocol.swift
pin a team ID. That pin is the security boundary for a root daemon, so it cannot
be derived at runtime — replace AU534DT7GN with your own team ID.
All the artwork is drawn from the crescent in
MenuBarIcon.swift, so the app icon, the
status item and the figures above cannot drift apart. Resources/AppIcon.icns
is generated on first build — delete it to regenerate after changing the mark.
The README figures come from the same tool:
swiftc -O Sources/Yami/MenuBarIcon.swift tools/GenerateArtwork.swift -o /tmp/makeicon
/tmp/makeicon --png docs/icon.png
/tmp/makeicon --states docs/menubar-states.pngThe core. Yami owns the mihomo process — spawn, supervise, restart on config
change, bounded restart budget on crashes. The control API is a unix socket
(-ext-ctl-unix) rather than a TCP port, so there is no second port to pick and
no API secret to store; file permissions are the access control.
The subscription. mihomo has no concept of a subscription, so this is most of
the actual logic: fetch with a mihomo User-Agent (providers serve HTML to
browser UAs), override the keys that decide how Yami reaches the core, strip
listeners the user was never told about, validate with mihomo -t, then swap
atomically. A bad subscription can never take a working core down.
The system proxy. Changing proxy settings needs root, so a bundled
SMAppService daemon does it over XPC — one approval at first use, silent
thereafter. Both ends verify the other's code signature. The helper writes all
network services in a single SCPreferences commit rather than shelling out to
networksetup.
Routing. Providers often ship a single MATCH rule and no real routing, so
Yami offers three positions: the subscription's own rules, the
Loyalsoldier set (mainland China
and LAN direct, ads rejected, everything else proxied), or Global. Switching
re-renders from the subscription already on disk — no round-trip to the provider,
and it works offline.
The config viewer. View Config opens a read-only window showing the YAML
the core actually loaded — the subscription with Yami's overrides applied, which
is the thing you would want to check. It is the only window in the app; the
default .yaml handler is usually Xcode, which is a poor way to read a config.
The interlock. A system proxy is only as good as the core behind it. If the core stops, the proxy comes down with it; if Yami crashes, the helper undoes the proxy when the XPC connection drops. Readiness means serving — a TCP connect to the proxy port, not just a live process.
DESIGN.md has the full reasoning, including the failures that
shaped it.
Every push to main replaces a single rolling prerelease tagged canary, so the
newest build is always at a stable URL:
https://github.com/skywardpixel/Yami/releases/download/canary/Yami.zip
Tagged releases are cut by pushing a v* tag, which publishes a real release
named for that version.
The version comes from git, so cutting a release is git tag v0.4.0 && git push origin v0.4.0 and nothing else — no file to bump. build.sh stamps three values
into Info.plist before signing (editing it afterwards would invalidate the
seal):
CFBundleShortVersionString |
the last v* tag, e.g. 0.3.0 |
CFBundleVersion |
commit count, monotonic across the history |
YamiSourceVersion |
exact provenance, e.g. 0.3.0-4-g1a2b3c4-dirty |
The About line shows the bare version for a tagged build and the full
description for anything else, so a local build never passes for a release. The
value committed in Resources/Info.plist is only a fallback for building from a
tarball with no repository.
Artefacts are only published when a Developer ID certificate is configured — see docs/RELEASING.md. Without one, CI still builds and tests, but publishes nothing: an ad-hoc build cannot be opened after download and its privileged helper rejects it, so the System Proxy toggle does not work. Shipping one is a trap rather than a convenience.
With signing configured, every build is notarized and stapled, so a download opens with no warnings and no terminal commands.
swift test # pure logic: config rendering, log parsing, port checks
./scripts/verify.sh # process lifecycle against a real coreverify.sh runs against a scratch YAMI_HOME with its own defaults domain, so
it never touches your real subscription, cache, or system proxy. It does need
port 7890, so it stops a running Yami and puts it back afterwards.
Yami is licensed under the GNU General Public License v3.0 — see LICENSE.
Copyright (C) 2026 skywardpixel
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
The bundled mihomo core is GPL-3.0 as
well, so a release is GPL-3.0 throughout. Its licence ships in
Contents/Resources/mihomo-LICENSE.txt and
docs/RELEASING.md records the exact upstream tag the bytes
came from.
~/Library/Application Support/Yami/ config.yaml, api.sock, mihomo's caches (0700)
~/Library/Logs/Yami/core.log