Skip to content

howar31/ac-caffeinate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AC Caffeinate

Make a MacBook's stay-awake behavior power- and display-aware — and take back control from a caffeinate you never asked for.

License Platform: macOS Made with Bash Stars Last commit Ko-fi

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

The problem

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.

How it works

Two facts make a small root daemon necessary:

  1. disablesleep is the only knob that keeps a Mac awake with the lid closed and no external display. (The ordinary pmset -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.
  2. pmset sleepnow is a forced sleep, so it overrides the idle-sleep assertions that caffeinate holds. Relying on the idle-sleep timer does not — with caffeinate running, 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
Loading

Behavior

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
Loading

Requirements

  • macOS on Apple Silicon (uses pmset, ioreg, launchd — all built in)
  • Administrator (sudo) to install, because setting disablesleep and installing a system LaunchDaemon require root.

Install

git clone https://github.com/howar31/ac-caffeinate.git
cd ac-caffeinate
sudo ./install.sh

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

Update

Pull the latest and re-run the installer — it's idempotent:

git pull
sudo ./install.sh

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

Uninstall

sudo ./uninstall.sh            # remove daemon + watcher, restore normal sleep
sudo ./uninstall.sh --purge    # also delete the log file

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

Verify

A quick read-only health check (no sudo):

./status.sh   # installed? running? in sync? and what it's doing right now

Or 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.log

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

Configuration

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

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

Caveats

  • On battery with no display in use, it forces sleep — overriding caffeinate and 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 disablesleep at 0 on 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 holding disablesleep 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 disablesleep and running a system daemon require root. Read the scripts before running them with sudo — they are short on purpose.
  • Tested on Apple Silicon MacBooks. It relies only on documented pmset/ioreg behavior, but power-management details vary across macOS versions.

License

MIT

About

Make a Mac's stay-awake behavior power- and display-aware, overriding an unwanted caffeinate (stay awake = AC ∪ active display; otherwise force sleep).

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

Contributors

Languages