You set Claude Code off on something long. A build, a download, an agent that will be a while yet. You close the lid, and macOS goes to sleep and takes the job with it.
Puer keeps the machine running with the lid shut, then sleeps after a delay you picked in advance — 30 minutes, 2 hours, 8 hours.
The timer is the point. This is a machine that ends up in a bag, so awake-forever is a choice you have to make deliberately, not the default.
Click the bowl in the menu bar. One flat list, one tick.
Off — sleep as usual
15 minutes
30 minutes
1 hour
✓ 2 hours
3 hours
4 hours
8 hours
12 hours
Never sleep
─────────────────
Open Puer Window…
─────────────────
Quit Puer
Picking a delay turns Puer on. Picking Off turns it off. Nothing else — no status line, no caption, no separate switch to disagree with the list. There used to be a "Keep awake" tick box and a delay submenu: two controls over one lid, and they could contradict each other. That is gone.
The glyph carries the state: a solid, steaming tea bowl while Puer is holding the Mac awake, a bare outline while it is off.
Open Puer Window… gives you the same list in a window, plus a status line that appears only when it has something to tell you — the live countdown while the lid is shut (Lid closed — sleeping in 12m 30s), the never-sleep confirmation, or a note that the status could not be read. The rest of the time it says nothing.
Puer also keeps a Dock icon, on purpose. When the menu bar is full macOS has no slot for the status item, and a menu-bar-only app would be unreachable.
macOS 13 or later on Apple Silicon. You will need an admin password, and the Xcode Command Line Tools for swiftc.
git clone https://github.com/startsevdev/puer.git
cd puer
sudo ./install.sh # the background service
./build-menubar.sh # compiles the app, renders the icon, sets launch-at-logininstall.sh needs sudo: the service changes a system power setting, and macOS only lets root do that. Nothing is downloaded and nothing is pre-built — the app is compiled on your machine from the source in this repo.
If the bowl never appears, /tmp/puer.err is the first place to look. If the Mac sleeps anyway, run pmset -g | grep SleepDisabled — with Puer on it should read 1 within about 15 seconds.
cd puer
sudo ./uninstall.shService, app, control files and log are removed, disablesleep is cleared, and the Mac goes straight back to stock behaviour.
Under the hood
What it does to your Mac. puer-daemon.sh runs as root under launchd (com.puer.daemon.plist) and polls the lid every 15 seconds with ioreg -r -k AppleClamshellState -d 1. Everything it does, it does by toggling one system flag, pmset -a disablesleep — the one mechanism that still holds off lid-close sleep on battery as well as on mains.
| State | What the daemon does |
|---|---|
| Puer on, lid open | Holds the flag at 1. The Mac will not sleep at all, though the display still dims and locks on its own schedule. |
| Puer on, lid closed, under your delay | Flag stays at 1, and the counter climbs by 15 s a poll. |
| Puer on, delay reached | pmset -a disablesleep 0, then pmset sleepnow. |
| Puer off | Re-asserts 0 whenever it finds the flag set, and nothing else. |
Opening the lid resets the counter. Switching Puer off takes effect at the next poll, within 15 seconds — nothing here is instant.
Each poll reads the flag back from pmset -g rather than trusting what it last wrote. So if something else on the system sets disablesleep behind its back, a switched-off Puer puts it back to 0 instead of silently leaving your Mac unable to sleep.
The app. menubar/main.swift (Swift/AppKit) never touches power settings itself. Three files in /Users/Shared carry the whole conversation:
| File | Meaning |
|---|---|
.puer-paused |
present means Puer is off |
.puer-limit |
the chosen delay in seconds; absent or not a number falls back to 3600 |
.puer-status |
written every poll as state;closed;limit;mode, e.g. closed;180;3600;active — the window's countdown is this file, not a guess |
The app reads PUER_SHARED from the environment when it is set, which is how the tests point it at a scratch directory.
"Never sleep" is not a separate mode: it is a delay so long it is never reached in a session, about ten years. It is the one choice that never stops on its own. Every other choice holds the Mac awake for exactly as long as you picked — which, at 8 or 12 hours, is still all afternoon in a bag.
Where the install puts things. install.sh copies puer-daemon.sh to /Library/Scripts as root:wheel, mode 755, bootstraps com.puer.daemon.plist into system, and clears any leftover disablesleep. build-menubar.sh compiles the app into ~/Applications/Puer.app, renders icon/AppIcon.icns via icon/make-icon.sh when the sources are newer, and registers ~/Library/LaunchAgents/com.puer.app.plist for launch at login. The three files in /Users/Shared are written at runtime, not by the installer.
Tests.
./tests/test-daemon.sh
./tests/test-menu.shNeither needs root and neither touches your real power settings. test-daemon.sh drives a rewritten copy of the daemon against stub pmset and ioreg in a temp directory, so the lid can be closed and time can pass without either actually happening — including the case where something else flips the flag mid-run. test-menu.sh compiles the app and runs it with --dump-menu against a scratch PUER_SHARED, then asserts the menu offers exactly one, correctly ticked choice. Both print PASS/FAIL per case and exit non-zero if anything failed.
Logs. /var/log/puer.log for the daemon, /tmp/puer.err for the app.
Repo layout
| Path | What it is |
|---|---|
puer-daemon.sh, com.puer.daemon.plist |
the poll loop and its launchd job |
menubar/main.swift |
the status item, the menu and the window |
icon/PuerIcon.swift, icon/make-icon.sh, icon/tea*.png |
the bowl: cream on a Claude-orange squircle for the app icon, template glyphs for the menu bar |
icon/PuerHero.swift, icon/make-hero.sh |
the banners on this page, drawn from the same bowl |
install.sh, uninstall.sh, build-menubar.sh |
setup and removal |
tests/test-daemon.sh, tests/test-menu.sh |
the two test scripts above |
MIT. See LICENSE.
