Last updated: 2026-09-03 · commit
5d400cf75
Hold the fans of an Apple Silicon Mac at a fixed speed while the provider is serving and the GPU is hot, so thermal throttling does not cut decode throughput. For operators who run the daemon on a Mac they own; the result is a root helper that engages above a GPU-temperature threshold and hands control back to macOS whenever the provider stops. Optional: inference never depends on it.
- Provider installed from a signed
Darkbloom.apprelease (installation).darkbloom fan enablerefuses to run from a bare binary: it locatesDarkbloom.app/Contents/Helpers/darkbloom-fan-helperrelative to its own executable and verifies the app, the CLI and the helper together (provider-swift/Sources/darkbloom/Fan/FanServiceManager+Install.swift,bundledHelperURL,verifyBundledApp). - The build advertises the capability:
scripts/install.shrequires the CLI marker string, the fileContents/Resources/darkbloom-runtime-capabilities/fan-helper-v1and the nested helper (signed toidentifier "io.darkbloom.fan-helper", TeamSLDQ2GJ6TL, mode0755) to be present together (verify_fan_helper_capability). - A Mac with SMC-controllable fans and GPU temperature sensors; fanless
machines (MacBook Air) report
unsupported. - Administrator rights:
enable,configure,disableanduninstallrun throughsudofrom the provider's user account.
-
Check what the hardware exposes (no root needed):
darkbloom fan diagnose # chip, fans, GPU sensors, supported: true/false darkbloom fan diagnose --jsonFan.diagnosticReport(provider-swift/Sources/darkbloom/Fan/FanCommand.swift) opens the SMC read-only throughAppleSMCBackend(provider-swift/Sources/DarkbloomFanCore/SMCBackend.swift) and lists every fan and GPU temperature key.supportedis true only when both lists are non-empty. -
Install and enable the helper:
sudo darkbloom fan enable # policy defaults (table below) sudo darkbloom fan enable --speed 70 --temperature 50
--speedis a percentage of each fan's maximum RPM;--temperatureis the engage threshold in °C, and the release threshold sits a fixed margin below it (FanPolicyConfiguration,provider-swift/Sources/DarkbloomFanCore/FanPolicy.swift). Defaults and the accepted speed range are incli-reference.md→darkbloom fan.FanServiceManager.enable(provider-swift/Sources/darkbloom/Fan/FanServiceManager.swift) requiresgeteuid() == 0and a non-rootSUDO_UIDnaming the provider account (error otherwise:run this command through sudo from the provider account (SUDO_UID is required)); it verifies the bundled helper's code signature, restores automatic control on any helper already loaded, then copies the helper to/Library/PrivilegedHelperTools/io.darkbloom.fan-helper, writes/Library/LaunchDaemons/io.darkbloom.fan.plistand/Library/Application Support/Darkbloom/fan-policy.json(0600, recordsenabled, the configured UID and policy), and bootstraps the LaunchDaemon. -
Serve. Every serve path — the LaunchAgent daemon,
--foregroundand--local— wraps its run inwithFanActivityLease(provider-swift/Sources/darkbloom/StartCommand+Modes.swift;provider-swift/Sources/darkbloom/FanActivityLease.swift). The provider asks the helper for a short lease and renews it well inside its lifetime (FanIPC.leaseDurationSeconds,renewalIntervalSeconds,provider-swift/Sources/DarkbloomFanProtocol/FanIPC.swift; values in runtime constants); a missing or failing helper is logged and ignored. -
Adjust later without reinstalling:
sudo darkbloom fan configure --speed 85 sudo darkbloom fan configure --temperature 55
At least one option is required (usage error otherwise).
configurerewritesfan-policy.jsonand, if the daemon is loaded,launchctl kickstart -k system/io.darkbloom.fanso it re-reads the policy.
FanDaemon (provider-swift/Sources/DarkbloomFanHelper/FanDaemon.swift) ticks
once per second and runs FanPolicyStateMachine
(provider-swift/Sources/DarkbloomFanCore/FanPolicy.swift):
| Rule | Value |
|---|---|
| Engage | GPU temperature ≥ --temperature for the engage sample count and a provider lease is active |
| Release | temperature ≤ the release threshold below --temperature for the release sample count, or the lease lapses |
| Speed while engaged | --speed % of each fan's maximum RPM, within the allowed range |
| No lease | waiting_for_provider: fans stay under macOS control even when hot |
| Lease lapse | Without a renewal inside leaseDurationSeconds (provider stopped, crashed, slept) the helper restores automatic control |
| Defaults | Trigger and release temperatures, speed and its range, sample counts, lease and renewal periods: cli-reference.md → Runtime constants |
| Safety | If a reading fails or the SMC refuses a write the helper restores automatic control and reports safety_override / error |
Modes reported by fan status are disabled, waiting_for_provider,
waiting_for_temperature, manual, safety_override, unsupported, error
(FanServiceMode). The helper journals ownership in
/Library/Application Support/Darkbloom/fan-session.json so a crash or reboot
mid-override is reconciled on the next start
(provider-swift/Sources/DarkbloomFanService/FanOwnershipRecovery.swift).
Only signed Darkbloom code can talk to the helper: the XPC service
io.darkbloom.fan (FanIPC.machServiceName) checks the connecting process's
Team ID against FanIPC.teamID = SLDQ2GJ6TL
(provider-swift/Sources/DarkbloomFanService/FanPeerAuthentication.swift),
and the helper itself is verified against the same Team-ID requirement before
installation.
darkbloom fan status # capability, installed, loaded, mode, policy, temperatures, fans
darkbloom fan status --json
sudo launchctl print system/io.darkbloom.fan | headWith the provider serving and the GPU above threshold for a few seconds,
mode moves from waiting_for_temperature to manual and the fan RPM column
rises to the configured percentage. Stop the provider (darkbloom stop); once
the lease lapses the mode returns to waiting_for_provider and macOS controls
the fans.
sudo darkbloom fan disable # restore automatic control; keep helper + LaunchDaemon installed
sudo darkbloom fan uninstall # disable, then remove plist, helper and fan-policy.jsonuninstall runs disable first and refuses to continue while
fan-session.json still records an override it could not undo
(ownership journal remains after restore; refusing to remove recovery material) — fix the SMC problem fan status shows, then retry.
darkbloom stop --uninstall and the installer never touch these root-owned
files, and the unprivileged self-updater cannot replace the helper: after
darkbloom update run sudo darkbloom fan enable again so the helper matches
the new app bundle.
| Symptom | Cause | Fix |
|---|---|---|
fan: this operation requires administrator privileges; rerun it with sudo |
Not root | Prefix with sudo |
fan: run this command through sudo from the provider account (SUDO_UID is required) |
Run from a root shell (sudo -i) or with SUDO_UID stripped |
Run sudo darkbloom fan … from the provider user's shell |
fan: signed fan helper was not found; reinstall Darkbloom |
CLI not running from Darkbloom.app/Contents/MacOS, or the helper is missing from the bundle |
Reinstall with scripts/install.sh; use ~/.darkbloom/bin/darkbloom |
fan: fan helper signature verification failed |
Helper not signed by Team SLDQ2GJ6TL |
Reinstall from the official release |
fan: fan control is unavailable: … / supported: false |
No SMC fans or GPU sensors | Nothing to do on this model |
| Fans never engage although the GPU is hot | No provider lease (fan status shows waiting_for_provider): the daemon is not running, or fewer than 3 consecutive samples were above threshold |
darkbloom status; darkbloom restart |
fan status mode error or safety_override |
SMC write refused or reading failed | sudo darkbloom fan disable, check fan diagnose, re-enable |
| Fan control stops after an update | Helper is not replaced by the unprivileged updater | sudo darkbloom fan enable |
- CLI reference —
darkbloom fanflag table and helper paths. - Installation — capability markers verified by
scripts/install.sh. - Troubleshooting — daemon lifecycle and
doctor. - Hardware requirements.