← Projects
Decaf

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
The Decaf app icon — a cup of coffee

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 Decaf menu — Claude Code working with three sessions, each with its own state, above manual hold controls and display options

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 icon states: idle, manual hold, agents working with a session count, and paused by a safety protection

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.

LayerSignalResolutionRole
L1 · hooksA helper binary Claude Code runs on SessionStart, UserPromptSubmit, Notification, Stop, SessionEnd, PostToolUseThe exact turnPrimary. Installed with explicit consent, deep-merged so your own hooks survive
L2 · file activityFSEvents on ~/.claude, 5-minute idle window~5 minutesZero-config fallback. Can’t see a turn boundary, only that something is still being written
L3 · CPU samplingproc_pid_rusageContinuousA 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 PostToolUse on 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, powerd drops 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.

Say hi 👋