macOS has had an alarm in the Clock app since Ventura. I found out the hard way that it doesn't ring once the Mac goes to sleep.
The workaround the internet offers is "turn off automatic sleep." That isn't a workaround. So I built Matuta — free, MIT, macOS 14+.
This post is about the parts that were actually interesting to build, including one API that would have shipped a bug I never would have caught by reading docs.
Waking a sleeping Mac
This part is one call.
import IOKit
import IOKit.pwr_mgt // not just `import IOKit`
// Wake two minutes early. If the alarm is sooner than that, wake at the alarm.
let wakeDate = date.addingTimeInterval(-120)
let targetDate = wakeDate > Date() ? wakeDate : date
guard targetDate > Date() else { return false }
let status = IOPMSchedulePowerEvent(targetDate as CFDate, appName, "wake" as CFString)
Two things cost me time here.
import IOKit alone does not expose IOPMSchedulePowerEvent. You need the IOKit.pwr_mgt submodule. The compiler just tells you the symbol doesn't exist, which sends you looking in the wrong place.
The two-minute lead is not arbitrary. A Mac that wakes at exactly 07:00:00 is not a Mac that makes sound at 07:00:00 — the audio stack needs a moment. Waking early and holding a power assertion until the alarm fires is the difference between "it rang" and "it rang on time."
This API is blocked in the App Store sandbox. That decided distribution for me: Developer ID signing plus Apple notarization, installed by dragging the app into Applications.
xcrun notarytool store-credentials matuta --apple-id <id> --team-id <team>
codesign --options runtime --timestamp ...
xcrun notarytool submit Matuta.zip --keychain-profile matuta --wait
xcrun stapler staple Matuta.app
Skip this and the first thing a user sees is a security warning. Bad first impression for an app whose whole pitch is reliability.
Ringing turned out to be about half the job
Once wake-from-sleep worked I assumed I was done. I wasn't.
A phone has one speaker. A Mac has opinions. Your output might be routed to the earbuds you fell asleep wearing. The system might be muted. The volume might be at 4%. The default device might be a monitor that's powered off.
Every one of those produces an alarm that "played successfully" and a person who overslept.
Every alarm app I looked at treats ringing as the whole job. So Matuta works in three layers:
- Before you sleep — a status row names anything that would stop the alarm from waking you, with a one-click fix for each. When everything's fine it stays quiet.
- At alarm time — output is forced back to the built-in speakers, the system is unmuted, volume is raised.
- Underneath — for any source it can't verify (Spotify, a browser tab), a backup tone plays concurrently. Something always makes noise.
The sound itself is one input field. Paste a Spotify or YouTube link, drop an mp3, point it at an internet radio stream, or type a song name — a parser works out which kind of source you handed it. No provider dropdown.
The half hour that changed the app
Here's the part I'd want to read.
To pick Spotify or Apple Music as a sound, the app needs Automation permission. There's an API to check it: AEDeterminePermissionToAutomateTarget. My plan was to call it, and show "permission needed, here's the settings pane" when it came back denied.
Before wiring up the UI I wrote a throwaway probe and ran it against every combination I could think of. The result:
If the target app isn't running, it returns -600 (procNotFound) — regardless of whether permission is actually granted.
Worse, these two states are indistinguishable:
- Spotify is installed, permission granted, but currently quit
- Spotify was never installed at all
Both are -600.
Think about what shipping that would have meant. Anyone who quits Spotify before bed — which is most people — gets a "no permission" warning every single night, on an app they open specifically to check that nothing is wrong.
And that's the real damage. Teach someone that your warnings are noise and they will scroll past the one that matters. A reliability app that cries wolf nightly is worse than no reliability app, because it actively trains the habit that defeats it.
So the rule became: while the target app is not running, don't assert anything about permission.
/// `AEDeterminePermissionToAutomateTarget` returns -600 (procNotFound) when the
/// target app isn't running, regardless of the real permission state. An installed
/// but quit music app and a never-installed Spotify both came back -600. So while
/// the target app is closed, we do not claim to know the permission state.
Thirty minutes of measuring. No amount of reading would have produced it — the documentation describes what the function returns when it can answer, not what it returns when it can't.
Alarm tones are synthesized, not shipped
All nine built-in tones are generated in code.
let wave = sin(phase1)
+ sin(phase2) * note.harmonicDecay * 0.4
+ sin(phase3) * note.harmonicDecay * 0.2
The reasoning is narrow and, I think, correct for this specific app: a file that doesn't exist can't fail to load. No missing resource, no broken bundle, no path that changed between OS versions. If the code runs, sound comes out.
A pleasant side effect is that the app is tiny.
The core doesn't produce sentences
The app ships Korean and English with instant switching, no restart. That fell out of one rule:
MatutaCore emits keys. The view resolves them against the current language.
MatutaCore → "preflight.muted"
View → "System is muted" / "시스템이 음소거입니다"
Two things came free.
Switching languages is just a re-render — nothing to reload, nothing to restart.
And core tests assert on keys, not prose. I can rewrite "System is muted" to "Your Mac is muted" without touching a single test. If you've ever broken twenty tests by improving one string, you know why that matters.
Structure
The organizing principle: decisions in the core, I/O in the app.
MatutaCore/ decisions and calculation (all unit tested)
├── NextOccurrence when an alarm fires next
├── Scheduler picks the next alarm, accounts for snooze
├── PlaybackChain source fallback, concurrent backup tone
├── PreflightEvaluator pre-sleep diagnosis
├── OmniboxParser input → source type
└── TonePattern tone synthesis
Matuta/ system I/O and drawing
├── AudioGuard output device, volume, mute
├── SystemWakeScheduler power event scheduling
└── Views/ SwiftUI screens
Anything that decides something is testable without a Mac in a particular state. Anything that touches CoreAudio or IOKit does no reasoning.
There's no Xcode project file — plain SwiftPM, with a shell script assembling the .app. The entire build is reproducible from a terminal.
git clone https://github.com/RAKKUNN/matuta.git
cd matuta
swift test
./Scripts/bundle.sh
open build/Matuta.app
What I haven't verified
I'm going to be specific here, because it's an alarm clock and someone might rely on it.
Wake-from-sleep for a normal alarm: verified. Real hardware, lid shut, overnight. It rang.
Wake-from-sleep after you hit snooze: not verified. The code path exists and has unit tests, but I have not measured it overnight. That's in the README too. If you're catching a flight, keep a second alarm.
The Apple Music source also just tells the Music app to play, so you get whatever is queued there. The backup tone fires either way.
Two decisions I'd defend
Spacebar dismisses. Snooze is deliberately mouse-only. Easy to turn off, harder to fall back asleep. That asymmetry is the entire design.
No dismissal missions — no math problems, no photo tasks. Plenty of apps do that. I wanted to solve "the alarm doesn't ring," not "I don't want to get up." Different problem.
Links
macOS 14+, free, MIT, no account, no analytics.
Happy to hear that it's broken, or that I've missed an app that already does all this. And if anyone has actually measured snooze wake-from-sleep, I'd genuinely like to know.
Top comments (0)