Skip to content

Reedtrullz/Vifty

Vifty

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.

Platform Swift CI Architecture License

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.

Vifty screenshot showing menu bar, fan controls, power telemetry, temperature sensors, and thermal history

Highlights

  • 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 viftyctl JSON commands to request bounded temporary cooling for builds/tests, with visible state and automatic restore.
  • Installer workflow — double-click Install Vifty.command, run make install, or build a reusable .pkg.
  • Safety defaults — RPM clamping, unsupported-hardware refusal, auto-restore on sensor loss, and unclean-exit recovery.
  • Debug helper CLIViftyHelper can probe SMC state and restore Auto from Terminal.

Why Vifty matters

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 viftyctl agent 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.

Supported scope

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.

Install and launch

Homebrew (recommended)

brew tap Reedtrullz/vifty https://github.com/Reedtrullz/Vifty
brew install --cask vifty

Then launch Vifty from Spotlight, Launchpad, or:

open /Applications/Vifty.app

From source

For normal local use:

  1. Double-click Install Vifty.command in this repository. It builds a release app, installs it, registers it with Launch Services, and launches Vifty.

  2. Or run:

    make install

After installation, start Vifty from Spotlight, Launchpad, Finder, or Terminal:

open /Applications/Vifty.app

make 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.

Build and verify

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 pkg

GitHub 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.

Power tracking

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:

  • IOPSCopyPowerSourcesInfo battery status and time estimates.
  • AppleSmartBattery registry values for voltage, signed amperage, capacity, cycles, condition, and temperature.
  • IOPSCopyExternalPowerAdapterDetails adapter 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.

Architecture

┌────────────────────────────────────────────────────────────┐
│ 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/.

Safety and privacy

  • 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.json with a .bak backup 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.

Optional: Harden XPC with your TeamID

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:

  1. Find your TeamID: make app CONFIGURATION=release SIGNING_IDENTITY="Apple Development" then codesign -dvvv .build/Vifty.app 2>&1 | grep TeamIdentifier
  2. Edit Sources/ViftyDaemon/main.swift — change teamIdentifier: nil to your TeamID on both XPCAllowedClient lines.
  3. 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.

Fail-safe recovery

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, direct ViftyHelper setFixed / auto writes require sudo.

  1. In the Vifty UI, select Auto in the Mode picker and click Apply.

  2. 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>
  3. To stop the privileged daemon while troubleshooting, unload it from launchd:

    sudo launchctl bootout system /Library/LaunchDaemons/tech.reidar.vifty.daemon.plist
  4. 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 CLI

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 dump

viftyctl agent CLI

viftyctl is bundled at:

/Applications/Vifty.app/Contents/MacOS/viftyctl

It 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 test

Safety 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 run restores 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.

Rate limiting

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 test

Daemon installation

The app bundles a LaunchDaemon plist. On first launch:

  1. SMAppService.register() attempts Login Items approval on macOS 13+.
  2. Fallback: administrator-prompted install via osascript to /Library/PrivilegedHelperTools/ plus launchctl 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.

Project structure

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

License

MIT. See LICENSE.