Keeping a Mac awake with the lid closed

Sleep assertions do not stop lid sleep. Here is how a root helper, an XPC connection and a long list of undo paths let agents keep working when you close the laptop.

BeyDock3 min read

Agents run for a long time. You start a task, close the laptop, and expect it to be done when you open it again. On a Mac that does not work by default, and the obvious fix does not work either. This build log explains why "With Lid Closed" needs a privileged helper, and how we made sure that helper can never leave your Mac in a bad state.

Why assertions are not enough

Keep Awake has three simpler modes: Off, Mac Only and Mac and Display. They use IOPM assertions, the same mechanism as caffeinate. An assertion stops idle sleep, and the system releases it when the process exits, so cleanup is free.

Closing the lid is different. An assertion does not prevent the sleep the lid triggers. The only thing that does is pmset -a disablesleep 1, and that needs root.

One common approach is a sudoers rule. That means any program you run can use the same commands without a password, and if the app crashes the setting stays on until a backup job reverses it. We did not want that trade. Our display, keyboard and audio layer is adapted from an open-source utility (MIT), with credit in the source. The root side is our own design.

A root daemon, tied to a connection

beydock-powerd is a launchd daemon inside the app bundle, registered with SMAppService.daemon. You approve it once in Login Items. The app talks to it over XPC.

The important property is that the daemon's state is tied to the connection. The sleep setting is held only while the app is connected. If the connection drops, because you quit the app or it crashed, the daemon undoes everything. It does the same on SIGTERM and at startup, in case a previous run left a marker behind. With no connection it exits after 30 seconds.

Marker files live in /Library/Application Support/BeyDock/power/<label>, one per build. A dev build and the installed app cannot switch off each other's setting, and a disablesleep you turned on yourself is left alone.

Checking who is connecting

A root process that accepts commands from any client is a privilege escalation waiting to happen, so the check runs both ways. The daemon requires the client to be signed by the same team and identity, and the app checks the daemon the same way.

The daemon also requires the client to have the hardened runtime. Without it, another process of the same user could inject code with DYLD_INSERT_LIBRARIES and pass the signature check. That is why the app is signed with -o runtime and an audio-input entitlement, so dictation still works.

For freezing other apps, the daemon only signals processes that belong to the connection's user and that BeyDock itself recorded. It stores each process's start time and boot session, so a reused process ID is never frozen by mistake. Ad-hoc builds cannot use any of this; the feature shows as unsupported.

Safety limits

Staying awake in a bag is how laptops get hot, so the daemon gives up on its own:

  • Below 20% battery, it releases the setting and the Mac sleeps. It comes back at 25%.
  • At a critical thermal state, it releases too.
  • Below 40% battery, or at a serious thermal state, it switches on low power mode instead.

Settings tells you to keep the Mac on a ventilated surface, not in a bag.

What happens when the lid closes

Staying awake is only half of it. Once the lid has been closed for two seconds, BeyDock can also:

  • turn the display and keyboard backlight to zero, then run pmset displaysleepnow;
  • mute the default audio output, and restore the same device when you open the lid;
  • set low power mode, using powermode on Apple silicon and lowpowermode on older Macs;
  • freeze chat apps (by default Slack, WhatsApp and Discord) with SIGSTOP and thaw them with SIGCONT.

Apple apps, BeyDock and Docker are never frozen. Settings › General › When the Lid Closes controls each action.

Brightness and volume are saved to lid-restore.json in the support folder. If BeyDock crashes while the lid is closed, the next launch restores them. We also restore the last brightness sample taken while the lid was open and you were at the machine, because a value read under a closing lid is unreliable.

Where it stands

Two places use this: Keep Awake › With Lid Closed, and Workflows › Even with the lid closed, which holds the Mac awake only while a workflow runs. This shipped in 0.3.0 and is still in early access testing.

Every agent. One dock.