Agents keep your Mac awake while they work.
This repo is the agent side of AllNighter/KeepAwake: hooks and instructions so coding agents hold an agent keep-awake switch for the duration of a task, then release it when they hand control back. No app code lives here.
Requires AllNighter / KeepAwake ≥ v1.1.0 with the local agent API on http://127.0.0.1:17893. With ≥ v1.2.0 holds are leased and expire on their own if a session dies without releasing (see below).
There are two user switches (display keep-awake, closed-lid keep-alive) and one agent switch. Combined effect:
effectiveDisplay = userDisplay OR agent
effectiveLid = userLid OR agent
Agent-off never overrides user-on (agent contributes only when on).
| User display | User lid | Agent | Effective display | Effective lid | Notes |
|---|---|---|---|---|---|
| OFF | OFF | OFF | off | off | Idle defaults |
| ON | OFF | OFF | on | off | User display only |
| OFF | ON | OFF | off | on | User lid only: display may sleep; system stays awake with lid closed |
| ON | ON | OFF | on | on | Both user switches on |
| OFF | OFF | ON | on | on | Agent force-enables both |
| ON | OFF | ON | on | on | Agent fills in lid |
| OFF | ON | ON | on | on | Agent fills in display |
| ON | ON | ON | on | on | Fully on |
- Agent ON force-enables display keep-awake and closed-lid keep-alive (
ORwith both user switches). - Agent OFF leaves effective state exactly as the user switches; it never turns off something the user has on.
- Leased (app ≥ v1.2.0): a hold not refreshed for 30 minutes expires on its own; the
PostToolUsekeepalive hook refreshes it as long as the session is actually working. On older apps holds are sticky (no TTL). App restart drops agent state; user switches remain as the app defines. - Escape hatches: menu Clear agent keep-awake, or
POST /agent/clear.
- macOS with AllNighter/KeepAwake ≥ v1.1.0
curl,bash,python3(for install merge and hook fallbacks)- Optional:
jq(hooks work without it)
./install.shFlags:
./install.sh --dry-run # plan only; no writes
./install.sh --hooks-only # Claude Code hooks only
./install.sh --instructions-only # instruction file onlyThe installer:
- Probes port
17893and points you at the AllNighter releases page if the app is absent. - Optionally installs hook scripts under
~/.claude/hooks/and deep-merges the hooks block into~/.claude/settings.json(backup first; append only; dedupe by(type, command); atomic write; JSON verified). - Optionally copies
instructions/AGENT-AWAKE.mdto~/.claude/AGENT-AWAKE.md. - Prints a verification line against
/status.
-
Copy scripts:
mkdir -p ~/.claude/hooks cp hooks/agent-awake-on.sh hooks/agent-awake-off.sh ~/.claude/hooks/ chmod +x ~/.claude/hooks/agent-awake-*.sh
-
Merge
hooks/settings-snippet.jsoninto~/.claude/settings.jsonunder the top-level"hooks"key. Expand~to your home path in thecommandfields (or use absolute paths). Prefer./install.sh --hooks-onlyso merge/dedupe/backup are handled for you.
Events:
| Event | Script | Role |
|---|---|---|
UserPromptSubmit |
agent-awake-on.sh |
acquire hold |
PostToolUse |
agent-awake-on.sh |
lease keepalive (refresh) |
Stop |
agent-awake-off.sh |
release |
StopFailure |
agent-awake-off.sh |
release |
SessionEnd |
agent-awake-off.sh |
release |
StopFailure matters: a turn that ends on an API error never fires Stop, so off must still run. Double-off is safe (unknown id → no-op 200).
PostToolUse matters: it re-POSTs /agent/on on every tool call, which on app ≥ v1.2.0 refreshes the hold's lease. A session that dies without any off-hook (crash, kill, closed terminal, interrupt) stops refreshing, and the app drops its hold ~30 minutes later instead of keeping the Mac awake forever.
A user interrupt also skips Stop → the hold stays until SessionEnd, a manual Clear, or lease expiry (app ≥ v1.2.0).
Paste or attach instructions/AGENT-AWAKE.md as persistent memory / CLAUDE.md / system-prompt “soul” text. That file is self-contained: when to call on/off, session id rules, and the exception that explicit user keep-awake requests use the user routes (/on, /off, /lid/*), not /agent/*.
Base: http://127.0.0.1:17893
| Action | Request |
|---|---|
| Agent on | POST /agent/on?id=<session> |
| Agent off | POST /agent/off?id=<session> |
| Agent status | GET /agent/status |
| Clear all agent | POST /agent/clear |
Fire-and-forget example:
curl -m 2 -s -o /dev/null -X POST "http://127.0.0.1:17893/agent/on?id=default"
curl -m 2 -s -o /dev/null -X POST "http://127.0.0.1:17893/agent/off?id=default"- id: platform session id (Claude Code:
session_idfrom hook stdin JSON). Fallback:default. Server-side this is a set of ids: duplicate/agent/oncalls with the same id collapse to one membership; a single/agent/off?id=releases that id; agent mode ends when the set is empty. - Leased (app ≥ v1.2.0): every
/agent/onalso refreshes that id's lease; a hold not refreshed withinleaseSeconds(default 1800) is dropped by the app.GET /agent/statusreportsageSecondsper id and the activeleaseSeconds. Still always send off when done — expiry is the safety net, not the mechanism. On app ≤ v1.1.x holds are sticky (no TTL). - App restart drops agent state (clears the set).
- Hooks never print to stdout (Claude Code treats stdout as context). They always exit 0; missing app must not delay the agent. Session ids in query strings are URL-encoded.
On app ≥ v1.2.0 an orphaned hold self-heals: with no keepalive arriving, it expires ~30 minutes after the session's last activity. To clear it immediately (or on older apps):
-
Menu bar: Clear agent keep-awake
-
Or:
curl -m 2 -s -o /dev/null -X POST "http://127.0.0.1:17893/agent/clear"
When agent keep-awake is active (non-empty agent id set), the menu bar shows a gold pulsing ring.
The cycle is 1 s: the glow builds for 0.8 s, fades sharply over 0.2 s, and a thin
black contrast ring shows at the trough. The GIF above is rendered from the app's
own --preview output, so it matches shipped pixels.
- Lid closed: closed-lid keep-alive while working can mean heat and battery drain. Do not leave a closed, plugged-in Mac in a bag. User lid-only (display off, lid on, agent off) keeps the system awake with the lid closed while still allowing the display to sleep.
- Orphaned holds: if the agent dies without
/agent/off, the hold expires on its own after ~30 minutes (app ≥ v1.2.0). Clear manually (menu or/agent/clear) if you don't want to wait, or on older apps. - App restart: agent state is dropped; do not assume agent on survived a relaunch.
MIT — see LICENSE.

