Make a MacBook's stay-awake behavior power- and display-aware — and take back control from a caffeinate you never asked for.
- On AC power → stay awake, even with the lid closed and no external display.
- On battery, while a display is in use → you're working; hand back to normal power management, never interrupt.
- On battery, with no display in use → you're away; sleep immediately, even if something is holding the Mac awake.
AI coding agents (Claude Code, Codex, and friends) and other background tools now routinely run caffeinate so they can keep working while you're away. That's fine on AC — but caffeinate holds a system power assertion (PreventUserIdleSystemSleep) that keeps the Mac awake on battery too. Close the lid, drop it in a bag, and it never sleeps — it just cooks.
You can't always see who started it, and killing the process kills the agent's work. What you actually want is: stay awake when I'm plugged in or actually using a screen; otherwise sleep, no matter what's holding the assertion.
Two facts make a small root daemon necessary:
disablesleepis the only knob that keeps a Mac awake with the lid closed and no external display. (The ordinarypmset -c/-b sleep <minutes>timers govern only idle sleep — they do nothing about lid-close sleep.) But it's a system-wide flag, not per-power-source: turn it on and the Mac never sleeps on battery either.pmset sleepnowis a forced sleep, so it overrides the idle-sleep assertions thatcaffeinateholds. Relying on the idle-sleep timer does not — withcaffeinaterunning, the timer never fires.
AC Caffeinate runs a tiny event-driven daemon that flips disablesleep on every power transition, and on battery — when no display is in use — issues a sleepnow that beats whatever caffeinate is holding open.
The forced sleep is the key move: caffeinate holds an idle-sleep assertion that no idle timer can overcome, but sleepnow is a forced sleep that ignores it.
sequenceDiagram
actor You
participant Agent as AI agent (caffeinate)
participant OS as macOS power mgmt
participant Daemon as ac-caffeinate
Agent->>OS: hold PreventUserIdleSystemSleep
Note over OS: idle-sleep timer blocked indefinitely
You->>OS: unplug charger (AC → battery)
OS-->>Daemon: pslog pulse
Daemon->>OS: pmset -a disablesleep 0
Daemon->>OS: count displays (ioreg) → 0
Note over Daemon: no display → you're away
Daemon->>OS: pmset sleepnow (forced)
Note over OS: forced sleep ignores the assertion
OS-->>You: Mac sleeps — no overheat in a bag
| Power source | Display in use? | Result |
|---|---|---|
| AC | — | disablesleep 1 — stays awake (full strength) |
| Battery | yes (lid open, or external display) | disablesleep 0 — normal power management, never forced to sleep |
| Battery | no (lid closed, no external display) | disablesleep 0 + immediate sleep |
"Display in use" is read from the live display panels: the built-in screen counts only while the lid is open, so a closed-lid laptop counts just its external displays. Zero means there's no screen you could be looking at.
It is event-driven (reacts to pmset -g pslog), not a polling loop, so it costs effectively nothing while idle.
The whole behavior as a state machine — disablesleep tracks the power source, and the forced sleep fires on the unplug transition when no display is in use:
stateDiagram-v2
direction LR
[*] --> AC: boot / plug in
[*] --> Battery: boot on battery
AC: On AC — disablesleep 1 (awake, even lid-closed)
Battery: On battery — disablesleep 0 (normal sleep)
AC --> Battery: unplug, no display in use → sleepnow (force sleep)
AC --> Battery: unplug, display in use → stay (you're working)
Battery --> AC: plug in → disablesleep 1
- macOS on Apple Silicon (uses
pmset,ioreg,launchd— all built in) - Administrator (
sudo) to install, because settingdisablesleepand installing a system LaunchDaemon require root.
git clone https://github.com/howar31/ac-caffeinate.git
cd ac-caffeinate
sudo ./install.shThis installs:
- the watcher script →
/usr/local/sbin/ac-caffeinate.sh - a LaunchDaemon →
/Library/LaunchDaemons/com.howar31.ac-caffeinate.plist - a log →
/var/log/ac-caffeinate.log
The daemon loads immediately and on every boot, and restarts itself if it ever exits.
Pull the latest and re-run the installer — it's idempotent:
git pull
sudo ./install.shinstall.sh re-copies the watcher and reloads the daemon in place (launchctl bootout then bootstrap), so the running daemon is replaced with the new version. No need to uninstall first.
sudo ./uninstall.sh # remove daemon + watcher, restore normal sleep
sudo ./uninstall.sh --purge # also delete the log fileUninstall resets disablesleep back to 0 (normal sleep). Skipping that would leave the global flag frozen at its last value — bringing back the never-sleeps problem.
A quick read-only health check (no sudo):
./status.sh # installed? running? in sync? and what it's doing right nowOr inspect the pieces yourself:
# On AC, this should print 1; on battery, 0:
pmset -g | grep SleepDisabled
# Watch transitions live, then plug/unplug the charger:
tail -f /var/log/ac-caffeinate.logTo test the away case: with the lid closed and no external display on AC (awake), unplug the charger — the Mac should sleep within a second or two, even if caffeinate is running.
Every path and the daemon label are environment-overridable. Defaults are the standard macOS locations.
| Variable | Default | Purpose |
|---|---|---|
LABEL |
com.howar31.ac-caffeinate |
LaunchDaemon label / plist filename |
PREFIX |
/usr/local |
watcher installs to $PREFIX/sbin |
DAEMON_DIR |
/Library/LaunchDaemons |
where the plist goes |
LOG_FILE |
/var/log/ac-caffeinate.log |
daemon stdout/stderr log |
Use the same overrides for install.sh and uninstall.sh:
sudo LABEL=com.example.accaf PREFIX=/opt/homebrew ./install.shThe watcher itself also accepts PMSET and IOREG overrides; they default to the absolute system paths (/usr/bin/pmset, /usr/sbin/ioreg) deliberately, since a root daemon should not trust an inherited $PATH.
- On battery with no display in use, it forces sleep — overriding
caffeinateand any other idle-sleep assertion. That is the whole point, but if you specifically want a closed, screen-less laptop to keep running on battery, this tool is not for you. - Clamshell (lid closed) on battery can still sleep — and this tool does not override that, on purpose. macOS only fully supports closed-lid (clamshell) operation while on AC power; on battery a lid-closed Mac may sleep even with an external display attached, and exactly when it does varies by Mac model, macOS version, and whether the display is attached directly or through a dongle. AC Caffeinate deliberately keeps
disablesleepat0on battery so that closing the lid or unplugging the last display reliably sleeps the Mac — that is the bag-overheat protection. Forcing it awake in clamshell-on-battery would mean holdingdisablesleep 1, which — because the daemon reacts only to power-source changes, not lid/display changes — would also keep a bagged, lid-closed laptop awake. For a reliable closed-lid setup, keep the Mac on AC (or work with the lid open on battery). - Setting
disablesleepand running a system daemon require root. Read the scripts before running them withsudo— they are short on purpose. - Tested on Apple Silicon MacBooks. It relies only on documented
pmset/ioregbehavior, but power-management details vary across macOS versions.