Native macOS menu-bar fan control and charger-power monitoring for Apple Silicon MacBook Pros. Vifty combines live thermals, fan RPM control, reusable temperature curves, and USB-C/MagSafe power telemetry in one local-first SwiftUI utility.
Vifty is built for local signed distribution, not the App Store. It uses private macOS SMC/HID interfaces for fan and sensor access, keeps data on-device, and refuses manual control on unsupported hardware.
- Menu bar cockpit — temperature, fan RPM, and power state at a glance.
- Three fan modes — Auto, Fixed RPM, and a 3-point Temperature Curve.
- Curve profiles — save, name, switch, overwrite, and delete fan curves; profiles persist across restarts.
- Hardware fan state — shows actual SMC Auto/Forced mode and target RPM when available.
- Live temperature panel — all SMC and HID sensors with source labels and highest-temperature tracking.
- Live power tracking — battery percentage, charge/drain watts, signed battery current, adapter wattage, negotiated USB-C voltage/current, health, cycle count, battery temperature, and USB-C PD profiles from local IOKit data.
- Thermal pressure — surfaces macOS thermal-pressure state alongside raw temperatures.
- Timed manual modes — Fixed RPM and Temperature Curve modes can automatically restore Auto after a selected duration.
- Power insights — estimates battery runtime from live drain and warns when plugged in but still draining.
- Telemetry history — keeps a local in-memory rolling history for recent temperature, fan, power, and thermal-pressure state.
- Privileged helper architecture — a LaunchDaemon/XPC helper owns root SMC writes so the app does not need repeated permission prompts.
- Helper health summary — distinguishes healthy helper fan data from helper errors, unreachable daemon state, and empty snapshots.
- Agent-friendly cooling leases — local agents can use bundled
viftyctlJSON commands to request bounded temporary cooling for builds/tests, with visible state and automatic restore. - Installer workflow — double-click
Install Vifty.command, runmake install, or build a reusable.pkg. - Safety defaults — RPM clamping, unsupported-hardware refusal, auto-restore on sensor loss, and unclean-exit recovery.
- Debug helper CLI —
ViftyHelpercan probe SMC state and restore Auto from Terminal.
Mac fan control has been dominated by proprietary closed-source tools for years. Vifty is different:
- Open-source and auditable — every SMC write path, RPM clamp, and safety check is visible. You can verify that fan control does exactly what it claims.
- Agent-safe by design — the
viftyctlagent CLI is the only open-source thermal management interface built for AI coding agents. Leases carry bounded durations, reasons, and idempotency keys; the daemon enforces expiry independently. No other Mac fan tool provides anything like this. - Combined fan + power + telemetry — instead of running separate tools for fan control, battery monitoring, and thermal pressure, Vifty gives you a single local-first utility with zero cloud dependencies.
- Privileged helper architecture — the root daemon owns SMC writes so the app never needs repeated permission prompts, and unprivileged direct AppleSMC writes are refused (fail-closed).
If you use Apple Silicon for builds, tests, or AI workloads, Vifty keeps your machine cool and your fan control auditable.
V1 targets Apple Silicon MacBook Pro models on macOS 15+. It intentionally excludes HDD/SSD S.M.A.R.T., Boot Camp, Windows support, analytics, cloud sync, and non-MacBook-Pro fan control. Unsupported Macs should remain under macOS automatic fan control.
brew tap Reedtrullz/vifty https://github.com/Reedtrullz/Vifty
brew install --cask viftyThen launch Vifty from Spotlight, Launchpad, or:
open /Applications/Vifty.appFor normal local use:
-
Double-click
Install Vifty.commandin this repository. It builds a release app, installs it, registers it with Launch Services, and launches Vifty. -
Or run:
make install
After installation, start Vifty from Spotlight, Launchpad, Finder, or Terminal:
open /Applications/Vifty.appmake install installs to /Applications/Vifty.app when writable and falls back to ~/Applications/Vifty.app otherwise. If you want a reusable installer file, run make pkg and open the generated .build/Vifty-<version>.pkg.
Requires macOS 15, Xcode 16, and Swift 6.
# Run the XCTest suite
swift test
# Build an ad-hoc-signed app bundle at .build/Vifty.app
make app CONFIGURATION=release
# Install the release app bundle
make install
# Optional: build an unsigned local installer package in .build/
make pkgGitHub Actions runs the same verification on every push to main, every pull request targeting main, and manual workflow_dispatch: Swift tests, release app bundle build, plist validation, ad-hoc code-signature verification, temporary install-script verification, and a zipped Vifty.app artifact upload.
The app bundle is signed ad-hoc with codesign --sign -. The local .pkg is unsigned and intended for local development/test installs; the app inside remains ad-hoc signed.
The power panel is inspired by projects like MacBook-Charger-Power-Indicator, but Vifty keeps the implementation inside its existing Swift/IOKit model layer. PowerInfoReader gathers:
IOPSCopyPowerSourcesInfobattery status and time estimates.AppleSmartBatteryregistry values for voltage, signed amperage, capacity, cycles, condition, and temperature.IOPSCopyExternalPowerAdapterDetailsadapter wattage, USB-C PD negotiation, manufacturer/model metadata, and advertised PD profiles.
The UI displays a compact menu-bar summary (96 W adapter, 16.9 W drain, etc.) plus a detailed Power panel next to the temperature sensors. Power telemetry is read locally and does not require the privileged fan helper. When live drain and capacity data are available, Vifty estimates time remaining and warns if the Mac is plugged in but the battery is still draining.
┌────────────────────────────────────────────────────────────┐
│ Vifty.app (SwiftUI menu bar + window) │
│ AppModel │
│ ├─ PowerInfoReader ── local IOKit power/battery data │
│ └─ FanControlCoordinator │
│ ├─ ViftyDaemonClient ── XPC ── root LaunchDaemon │
│ └─ RealMacHardwareService ── local SMC fallback │
│ │
│ ViftyDaemon │
│ tech.reidar.vifty.daemon ── AppleSMC IOKit │
└────────────────────────────────────────────────────────────┘
| Package | Type | Role |
|---|---|---|
Vifty |
executable | SwiftUI menu bar app and main window |
ViftyCore |
library | Models, SMC client, fan coordinator, power snapshots, daemon protocol |
ViftyDaemon |
executable | Privileged XPC daemon that reads/writes fan SMC keys as root |
ViftyHelper |
executable | CLI for direct SMC probing and emergency fan restoration |
ViftyCtl |
executable | Agent-friendly JSON CLI for bounded cooling leases |
ViftyPrivateIOKit |
library | C/IOKit bridge for HID temperature sensors |
Data flow: the app polls every 2 seconds. Fan control resolves the selected mode into per-fan RPM targets, then writes through the daemon when available. Power telemetry is read directly from local macOS IOKit dictionaries. Curve profiles are persisted as JSON in ~/Library/Application Support/Vifty/.
- Fan RPM targets are clamped to
[minRPM, maxRPM]per fan. - Hardware must be Apple Silicon + MacBookPro before manual fan control is enabled.
- Manual fan modes can be time-limited so Vifty restores Auto automatically.
- The UI distinguishes Vifty's selected mode from the hardware-reported SMC mode when that SMC key is available.
- If temperature sensors disappear mid-curve, Vifty restores Auto.
- An unclean-exit marker (
~/Library/Application Support/Vifty/manual-control-active) is written while manual control is active; the next launch restores Auto before continuing. - Curve profiles are stored in
~/Library/Application Support/Vifty/curve-profiles.jsonwith a.bakbackup before each save. - Power, thermal, and telemetry-history data stay on the Mac. The telemetry history is in-memory only; there are no analytics, accounts, network uploads, or cloud dependencies.
By default Vifty accepts any ad-hoc-signed binary with the correct signing identifier over XPC — this keeps the project buildable by anyone who clones it. If you have an Apple Developer account, you can lock the daemon to only accept binaries signed by your team:
- Find your TeamID:
make app CONFIGURATION=release SIGNING_IDENTITY="Apple Development"thencodesign -dvvv .build/Vifty.app 2>&1 | grep TeamIdentifier - Edit
Sources/ViftyDaemon/main.swift— changeteamIdentifier: nilto your TeamID on bothXPCAllowedClientlines. - Build with your identity:
make app CONFIGURATION=release SIGNING_IDENTITY="Apple Development"
This prevents other processes on your Mac from impersonating Vifty or viftyctl on the XPC bus.
If manual fan control misbehaves, restore Auto before trying anything else:
AppleSMC call failed with kIOReturnNotPrivileged (-536870207)means macOS rejected a direct fan write because it was not running through the privileged helper/root path. In the app, use Reinstall Helper and approve the helper if System Settings asks. From Terminal, directViftyHelper setFixed/autowrites requiresudo.
-
In the Vifty UI, select Auto in the Mode picker and click Apply.
-
If the UI is unavailable, use the helper CLI from the repo root after building release binaries. First inspect supported fans and their limits:
sudo .build/release/ViftyHelper probeLocal
Then restore Auto for each fan ID using its reported minimum and maximum RPM:
sudo .build/release/ViftyHelper auto 0 <minRPM> <maxRPM> sudo .build/release/ViftyHelper auto 1 <minRPM> <maxRPM>
-
To stop the privileged daemon while troubleshooting, unload it from launchd:
sudo launchctl bootout system /Library/LaunchDaemons/tech.reidar.vifty.daemon.plist
-
If fan state is still unclear, reboot macOS so the firmware/system controller and launchd return to normal startup state.
Do not run manual fan control on unsupported hardware.
ViftyHelper probe # Full hardware snapshot via daemon
ViftyHelper probeLocal # Direct SMC read (no daemon)
ViftyHelper readKey <key> # Read raw SMC key, e.g. F0Ac
ViftyHelper setFixed <id> <rpm> <min> <max>
ViftyHelper auto <id> <min> <max>
ViftyHelper smcDiagnostics # IOKit service discovery dumpviftyctl is bundled at:
/Applications/Vifty.app/Contents/MacOS/viftyctlIt is designed for local AI/coding agents and shell automation. It exposes structured JSON and bounded workload leases rather than arbitrary raw SMC writes:
viftyctl status --json
viftyctl capabilities --json
viftyctl prepare --workload build --duration 45m --max-rpm-percent 75 --reason "Swift release build" --idempotency-key "$(uuidgen)" --json
viftyctl restore-auto --reason "workload complete" --json
viftyctl run --workload test --duration 20m --max-rpm-percent 70 --reason "swift test" -- swift testSafety rules:
- Agent control is local-only through the signed CLI and privileged daemon.
- Every prepare request carries a bounded duration and reason; the CLI supplies a default reason when one is omitted.
- RPM targets are computed from each fan's min/max range and clamped by policy.
- User Auto restore wins over an active agent lease.
viftyctl runrestores Auto on normal child launch/exit; if the wrapper is killed or crashes, the daemon-owned lease monitor is the safety fallback.- Sensor loss, unsupported hardware, helper uncertainty, or critical thermal pressure refuses or restores control.
A 30-second cooldown (configurable via prepareCooldownSeconds in AgentControlPolicy) prevents rapid prepare/restore cycles from thrashing fan RPM. Repeated calls within the window return prepareRateLimited error with retry-after metadata.
For human use, --force retries once after the cooldown expires:
viftyctl prepare --workload build --duration 45m --max-rpm-percent 75 --force --reason "build" --json
viftyctl run --workload test --duration 20m --max-rpm-percent 70 --force -- swift testThe app bundles a LaunchDaemon plist. On first launch:
SMAppService.register()attempts Login Items approval on macOS 13+.- Fallback: administrator-prompted install via
osascriptto/Library/PrivilegedHelperTools/pluslaunchctl bootstrap.
The Reinstall Helper button retries this flow. The bundled LaunchDaemon plist uses BundleProgram, which the installer patches with PlistBuddy so launchd points at the daemon inside the installed app bundle.
Vifty/
├── Install Vifty.command # Double-click installer launcher
├── Makefile # app/install/pkg targets
├── Package.swift
├── Resources/
│ ├── Info.plist
│ └── tech.reidar.vifty.daemon.plist
├── scripts/
│ ├── build-installer-pkg.sh
│ └── install-vifty.sh
├── Sources/
│ ├── Vifty/ # Main app target
│ ├── ViftyCore/ # Shared models, fan control, SMC, power telemetry
│ ├── ViftyCtl/ # Agent-friendly JSON CLI
│ ├── ViftyDaemon/ # Privileged XPC daemon
│ ├── ViftyHelper/ # CLI helper
│ └── ViftyPrivateIOKit/ # C IOKit bridge
└── Tests/
└── ViftyCoreTests/ # XCTest suite
MIT. See LICENSE.
