★
Decaf
A macOS menu bar app that keeps your Mac awake only while a coding agent is actually working — and lets it sleep the moment the agent is waiting on you.
- Type
- Other
- Role
- Solo
- Status
- Active
- Tech
- Swift 6 SwiftUI AppKit IOKit FSEvents Swift Package Manager XcodeGen Swift Testing GitHub Actions Homebrew Cask
- Started
- Aug 2026
Decaf is the caffeinate command as a menu bar app that knows when
to stop. It holds your Mac awake while a coding agent is actually
working, and lets it sleep the moment the agent finishes — or is
only waiting on you.
The one rule: a twenty-minute tool call is work. A prompt sitting unanswered on your screen is not.
The whole app is this menu: what’s running, and what you can do about it.
Why this exists
Every keep-awake app in this category is a switch you have to remember to turn off. Leave one on overnight and your Mac is awake until you notice. Turn it off too eagerly and a long build dies halfway through.
The interesting part isn’t preventing sleep — that’s one IOKit call. It’s deciding: telling “the agent is thinking” apart from “the agent is waiting for you,” reliably enough that neither the false positive nor the false negative is common.
Install
brew install --cask AlanY1an/decaf/decaf
Or grab the signed, notarized DMG from the latest release. Requires macOS 14 or later.
Four menu bar states. The Mac sleeps normally in the first one.
How it decides
Three detection layers, most precise first. They aren’t alternatives — the lower ones cover the gaps in the higher ones.
| Layer | Signal | Resolution | Role |
|---|---|---|---|
| L1 · hooks | A helper binary Claude Code runs on SessionStart, UserPromptSubmit, Notification, Stop, SessionEnd, PostToolUse | The exact turn | Primary. Installed with explicit consent, deep-merged so your own hooks survive |
| L2 · file activity | FSEvents on ~/.claude, 5-minute idle window | ~5 minutes | Zero-config fallback. Can’t see a turn boundary, only that something is still being written |
| L3 · CPU sampling | proc_pid_rusage | Continuous | A witness, never a trigger. It can only contradict a hold, never start one |
Two inferences carry most of the weight:
- Permission prompts. Claude Code emits nothing when you click
“allow”, so the proof of approval is the tool’s own completion —
a
PostToolUseon a session marked awaiting permission puts it straight back to working. A tool can’t finish while its own dialog is unanswered, which is what makes that sound. - Wait signals. When an agent declares it will wait — a scheduled wake-up, a monitor with a timeout — that declaration lands in the transcript as a tool-use record. Decaf reads the stated instant and waits with it instead of sleeping through the gap.
A twenty-minute build writes nothing and prints nothing. All four witnesses have to agree a working session has gone quiet before the hold drops, which is what carries a session through it.
The hook binary’s contract
A misbehaving hook breaks Claude Code itself, so decaf-bridge
is deliberately the smallest thing in the repo: read stdin, parse
five head fields, connect a UNIX socket, write one JSON line, exit
0. Any failure exits 0 silently — no stdout (it would be injected
into your conversation as context), no stderr, never a non-zero
code, all of it inside a 90 ms watchdog. It’s allowed to import
exactly one module, and a CI script checks the linked-library
whitelist so that stays true.
Safety rails
- Every assertion carries a 30-minute
TimeoutActionRelease— if Decaf crashes,powerddrops the hold rather than leaving a Mac awake forever. - Renewal is create-then-release at half the timeout, never
IOPMAssertionSetProperty. - Low Power Mode, fast user switching, and a low battery all release the hold. Closing the lid always wins.
Privacy
Decaf watches ~/.claude, which deserves a straight answer: it
makes no network requests — no telemetry, no analytics, not
even an update check — and it never reads your conversation.
It reads timestamps, session ids and token counts. The parsers are
closed enums with tests pinning their fields, so widening them is
a failing build rather than something a code review has to catch.
You see the exact JSON it will merge into ~/.claude/settings.json
before it writes anything, and Settings can undo it.
Shipping it
v0.1.0 went out signed and notarized, with a Homebrew tap
alongside the DMG so brew install --cask works out of the box.
The Xcode project is generated from an XcodeGen manifest and
gitignored; CI runs the package tests on every push.
It is not on the Mac App Store, and won’t be — the sandbox makes what Decaf does impossible, not merely inconvenient.
Name
Everything in this category is named after the stimulant. This one
is named after knowing when to stop. The subtitle keeps the word
caffeinate because that’s the command you already know, but the
app deliberately doesn’t share the name, so it can never shadow
/usr/bin/caffeinate.