builtinctl is a small macOS command-line utility that logically disables the built-in Mac display while an external display is active, then restores it when the last external display disappears. It does not fake this with brightness or screen blanking.
Important: builtinctl uses the undocumented macOS private symbol
CGSConfigureDisplayEnabled. It may break after a macOS update, has no compatibility guarantee from Apple, and is unsuitable for Mac App Store distribution. Test it after major macOS upgrades.
The supported binary release currently targets Apple Silicon and macOS 13 or later. It does not require Xcode or the Xcode Command Line Tools:
brew install d1scolor/tap/builtinctlThen keep an external display attached and follow the safe first-run procedure below before enabling automatic startup.
- macOS 13 or later
- Apple Silicon is the initial target
- Xcode Command Line Tools / Swift 5.9+
swift build -c release
.build/release/builtinctl statusNo root access, network service, telemetry, or third-party runtime dependencies are used.
builtinctl status show display, automation, and private API state
builtinctl on logically enable the built-in display
builtinctl off disable it (refuses unless an external display is active)
builtinctl auto run automatic switching in the foreground
builtinctl suspend activate the kill switch and immediately restore the panel
builtinctl recover alias for suspend, intended for emergency recovery
builtinctl resume remove the kill switch and explicitly re-arm a running daemon
builtinctl watch print CoreGraphics display events without changing displays
builtinctl test-off safely test disable with a 15-second confirmation timeout
builtinctl install-agent install and start a suspended per-user LaunchAgent
builtinctl restart-agent safely restart on the current CLI, preserving suspension
builtinctl uninstall-agent restore, suspend, and remove the LaunchAgent
builtinctl purge restore, remove automation, configuration, and logs
With the lid open, automatic mode first attempts to enable the panel and enforces a 60-second startup recovery window. An explicit resume re-arms a running daemon immediately, clearing the remaining startup grace and any reconnect latch. Automatic mode then uses CoreGraphics callbacks with a two-second safety watchdog. All changes are session-only; logout or restart should discard them. SIGINT, SIGTERM, SIGHUP, and SIGQUIT restore the built-in display before exit when the lid is open; clamshell mode defers restoration until the next open-lid session.
Automatic mode keeps its event loop isolated from private display mutations. Each transition runs through a short-lived internal helper with a five-second timeout and a cross-process mutation lock. Disabling requires a freshly confirmed active external display, a durable built-in recovery ID, and an inactive kill switch. These conditions are checked again after beginning the transaction and immediately before the private call. The helper verifies the result and reverses an unsafe post-condition. After automatic restoration, a recovery latch prevents another disable until an absent external and subsequent reconnect have both been observed.
On macOS 26, unplugging the last USB-C/DisplayPort Alt Mode monitor while the built-in is logically disabled creates a synthetic active CoreGraphics display. Its vendor is 756e6b6e (unkn) and model is 76697274 (virt); its display ID is not stable. Enumeration excludes only that exact vendor/model identity. This allows the lightweight CoreGraphics watchdog to detect removal without system_profiler polling.
The last verified built-in display ID is cached at ~/.config/builtinctl/builtin-display-id. CoreGraphics removes a logically disabled panel from its online inventory, so this validated ID lets a later builtinctl on process find it again. The cached value is used only while CGDisplayIsBuiltin still confirms it is a built-in display.
Before any disable, builtinctl durably writes:
~/.config/builtinctl/builtin-disabled
The marker includes the kernel boot time and macOS audit login-session ID. It is removed only after the built-in is verified active. The LaunchAgent restarts only after an unsuccessful exit.
If a restarted process finds a marker from the same boot and login session, it restores the panel, creates a crash suspension carrying the same session identity, and requires an explicit builtinctl resume. This prevents an in-session crash loop. If the marker or crash suspension belongs to an earlier boot or login session, the process restores the panel and automatically resumes after the normal 60-second startup grace. A legacy, corrupt, or unverifiable marker takes the conservative same-session path. An existing user-created suspension always remains in force.
Only one auto process may hold the automation lock. All display mutations also share a separate lock, preventing a late off helper from racing crash recovery. The watchdog uses CoreGraphics' public online/asleep state to distinguish a connected sleeping external from a physical unplug. A dispatch-based IOKit power watcher pauses topology mutations as soon as macOS proposes idle sleep, acknowledges sleep immediately, and keeps automation paused until hardware has completed waking. AppKit workspace notifications remain as a secondary display-sleep signal. After a completed wake, automatic mode restores the built-in and starts a fresh 15-second recovery window.
Clamshell mode remains owned by macOS. builtinctl reads AppleClamshellState and subscribes to root power-domain lid notifications. While the lid is closed, automatic mode performs no display mutations and defers any pending recovery. Opening the lid starts a three-second topology settling window before normal policy resumes. Manual display changes and commands that require verified restoration, including suspend, agent lifecycle operations, and purge, require the lid to be open.
Keep an external display attached and verify each step:
builtinctl status
builtinctl test-off
builtinctl autoConfirm that unplugging the external display and pressing Ctrl-C both restore the panel. Automatic mode honors the persistent kill switch at ~/.config/builtinctl/disabled.
After hardware testing:
builtinctl install-agentThis installs a persistent launcher and fallback executable under ~/Library/Application Support/builtinctl/bin, installs ~/Library/LaunchAgents/io.github.builtinctl.auto.plist, and bootstraps it. When installed through Homebrew, the launcher records Homebrew's stable opt path and creates a private versioned runtime copy at each daemon start. Installation first restores the built-in and creates the suspension sentinel, so switching is not silently enabled. Explicitly opt in with:
builtinctl resumeLogs are written under ~/Library/Logs/builtinctl. The agent uses conditional KeepAlive with SuccessfulExit=false, not unconditional restart.
The running daemon is not interrupted during a Homebrew upgrade. At its next start—such as the next login or reboot—the persistent launcher automatically snapshots and runs the current Homebrew version. No agent reinstall is required:
brew upgrade builtinctl
builtinctl statusTo apply an update immediately, safely restore and restart the agent while preserving whether automation is enabled or suspended:
builtinctl restart-agentAgents installed before version 0.1.4 require one final restart-agent to migrate their executable and LaunchAgent configuration. Subsequent Homebrew upgrades apply automatically at the next daemon start. status reports the CLI version, running agent version, and whether an update is pending.
Normally, run either:
builtinctl suspend
builtinctl recoverBoth commands create the kill switch and immediately attempt restoration. If an external is available, reconnect it and run either command. If necessary, log out or restart; configuration is committed only for the current session. Remove the managed LaunchAgent with:
builtinctl uninstall-agentAs a last resort from macOS Recovery, remove the user's builtinctl LaunchAgent from the Data volume before logging in again.
brew uninstall builtinctl removes only the Homebrew-managed CLI. It does not unload a separately installed LaunchAgent or remove builtinctl's per-user files.
To remove automation while retaining the suspension sentinel and logs for a possible reinstall:
builtinctl uninstall-agent
brew uninstall builtinctlFor complete removal, including configuration and logs:
builtinctl purge
brew uninstall builtinctlpurge permanently deletes builtinctl's per-user state and logs. It first suspends automation and verifies that the built-in display is active, then unloads the LaunchAgent and removes its plist, copied executable, configuration, and logs. If restoration or agent removal fails, purge stops before deleting the remaining safety state.
If Homebrew was uninstalled first, the LaunchAgent's safety copy normally remains available:
"$HOME/Library/Application Support/builtinctl/bin/builtinctl" purgeIf that executable is absent or is an older version without purge, reinstall the CLI and complete cleanup in the safe order:
brew install d1scolor/tap/builtinctl
builtinctl purge
brew uninstall builtinctlswift testDisplay mutation requires real hardware testing; the fail-open policy is covered by unit tests.