# claude-awake: keep your Mac awake (even lid-closed) while Claude Code is working — heartbeat hooks + pmset watchdog

> Source: <https://gist.github.com/whitefoxx/3579ba4370bdc82eacb0e7ab1f29057b>
> Published: 2026-08-02 05:07:59+00:00

|
#!/bin/bash |
|
# claude-awake-daemon.sh — the SINGLE authority on `pmset disablesleep`. |
|
# |
|
# Every POLL seconds it looks at two things and decides whether the Mac may sleep: |
|
# 1. the per-session state files written by claude-awake-hook.sh |
|
# (a file is "active" only if it was refreshed within STALE_MIN — a heartbeat, |
|
# so a crashed/killed Claude can't pin sleep open forever) |
|
# 2. the thermal state (CPU_Scheduler_Limit / CPU_Speed_Limit < 100 = throttling) |
|
# |
|
# ≥1 active session AND not overheating → disablesleep 1 |
|
# (the lid can be closed and the Mac stays awake to finish the task) |
|
# otherwise (all idle/closed, OR too hot) → disablesleep 0 |
|
# (normal sleep — closing the lid sleeps it, so the battery doesn't drain; |
|
# overheat → it's allowed to sleep & cool instead of cooking in clamshell) |
|
# |
|
# Idle Claude instances (open but not running a task) keep NO active file, so they |
|
# do NOT hold sleep open. Restores sleep if the watchdog itself exits. |
|
# |
|
# Run it via the LaunchAgent (RunAtLoad + KeepAlive). Needs passwordless sudo for |
|
# the two exact pmset commands — see the setup notes printed by the assistant. |
|
|
|
STATE_DIR="$HOME/.claude/awake-state" |
|
LOG="$HOME/.claude/awake.log" |
|
POLL=20 # seconds between checks |
|
STALE_MIN=30 # a session file not refreshed within this many minutes = stale → ignored |
|
COOL_LIMIT=100 # CPU_Scheduler_Limit / CPU_Speed_Limit below this = throttling (hot) |
|
|
|
mkdir -p "$STATE_DIR" 2>/dev/null |
|
log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >>"$LOG"; } |
|
|
|
applied="" # last value we actually pushed to pmset (avoid redundant calls) |
|
set_sleep() { # $1: 1 = disable sleep (stay awake), 0 = enable sleep (allow sleep) |
|
[ "$applied" = "$1" ] && return |
|
if sudo -n /usr/bin/pmset -a disablesleep "$1" 2>>"$LOG"; then |
|
applied="$1" |
|
log "disablesleep=$1 $([ "$1" = 1 ] && echo '→ staying awake (lid-close OK)' || echo '→ sleep allowed')" |
|
else |
|
log "pmset FAILED — set up passwordless sudo for pmset (see /etc/sudoers.d/claude-pmset)" |
|
fi |
|
} |
|
|
|
overheating() { # 0 = hot/throttling, 1 = ok |
|
local therm field val |
|
therm="$(pmset -g therm 2>/dev/null)" |
|
for field in CPU_Scheduler_Limit CPU_Speed_Limit; do |
|
val="$(printf '%s' "$therm" | sed -n "s/.*$field[[:space:]]*=[[:space:]]*\([0-9]*\).*/\1/p" | head -1)" |
|
[ -n "$val" ] && [ "$val" -lt "$COOL_LIMIT" ] && return 0 |
|
done |
|
return 1 |
|
} |
|
|
|
active_count() { find "$STATE_DIR" -type f -mmin "-$STALE_MIN" 2>/dev/null | wc -l | tr -d ' '; } |
|
|
|
# Never leave sleep disabled if the watchdog dies / is restarted. |
|
trap 'sudo -n /usr/bin/pmset -a disablesleep 0 2>/dev/null; log "watchdog exit → sleep ENABLED"; exit 0' INT TERM |
|
|
|
log "watchdog up (poll ${POLL}s, stale ${STALE_MIN}m, cool<${COOL_LIMIT})" |
|
while true; do |
|
if [ "$(active_count)" -gt 0 ] && ! overheating; then |
|
set_sleep 1 |
|
else |
|
set_sleep 0 |
|
fi |
|
find "$STATE_DIR" -type f -mmin "+$STALE_MIN" -delete 2>/dev/null # reap stale markers |
|
sleep "$POLL" |
|
done |
