Hammerspoon custom notification sounds on Mac, and where they run out

hs.notify can't do this. It's send-only: its soundName parameter chooses the sound for notifications Hammerspoon itself posts, and has no effect on a banner from Slack, Mail, or any other app. Almost everyone looking for a Hammerspoon notification sound finds soundName first, writes a rule, hears nothing change, and assumes they've made a mistake. They haven't — it's the wrong module for reacting to notifications.

Two modules can react to another app's notification, and only one of them works for the notifications people actually care about. Below: what each one does, about twenty-five lines of Lua that plays a sound when a chosen app's banner appears, and the list of things that go wrong once you're the one maintaining it.

Hammerspoon is a genuinely good tool and this page isn't an argument against it. If you already run it, you can solve this yourself today, and the code is right here.

What can react to other apps' notifications in Hammerspoon

Reacting to a notification means noticing that another process posted one. macOS exposes two routes for that, and Hammerspoon wraps both.

1. hs.distributednotifications

hs.distributednotifications listens on the system-wide NSDistributedNotificationCenter. It catches messages an app broadcasts on purpose — the classic example is watching for the screen locking, or for a media player publishing the track that just started. Some apps announce useful things this way and it costs you three lines to listen.

The catch is that this channel has nothing to do with the banners in Notification Center. A distributed notification is inter-process broadcast; a banner is UI. An app posting one doesn't post the other, and most modern Mac apps that ping you never touch NSDistributedNotificationCenter at all. Hammerspoon's own documentation is blunt about the mechanism: it's expensive in IPC terms, and delivery isn't guaranteed.

Limit: it only sees apps that deliberately broadcast, which most banner notifications don't. You can't make an app broadcast, so no amount of Lua gets Slack messages out of this module.

2. hs.axuielement.observer

hs.axuielement is Hammerspoon's binding to the macOS Accessibility API, and it's the route that can genuinely react to another app's banner: banners are Accessibility elements with readable text, and an observer can fire a callback when one appears.

That's the same layer Chirpy uses. It observes the text already visible in Notification Center banners — no private frameworks, no Slack API, no screen capture, nothing leaving the Mac. The difference between the two isn't the mechanism. It's who keeps it working when macOS moves the element tree.

Where the DIY route runs out

Getting a first sound to play is an evening. Keeping it playing is the part that doesn't end, and the costs are specific.

Limit: the element paths aren't API. Nothing obliges Apple to keep Notification Center's Accessibility tree the same shape, and a point release that moves the text one level deeper turns your working config into silence with no error in the console.

Limit: there's no rules UI. Adding a sound for one person means editing Lua and reloading, which is fine for you and a non-starter for anyone else on your Mac.

Limit: matching on banner:find("Slack") is a substring test on whatever text happens to be in the element. Per-channel, per-sender, and keyword rules mean parsing that text yourself, including the cases — grouped banners, threads, reactions, the app name appearing in someone's message — and deciding which rule wins when two match.

Limit: there's no sound library. You're sourcing, trimming, and normalising audio files by hand, and per-rule volume means loading each sound and setting :volume() on it.

Limit: you're the maintainer now. Every macOS release is your regression test, and the failure mode is a silent one you might not notice for a week.

None of that is Hammerspoon's fault. It exposes the Accessibility API faithfully; the work is in everything on top of it.

Hammerspoon vs Chirpy, row by row

CapabilityHammerspoonChirpy
Reacts to any app's bannerYes, via an hs.axuielement observer you writeYes
Rules UINo — rules are a Lua table you edit and reloadYes, a rule list with app, text, sound, and volume
Per-channel, sender, and keyword rulesYes, if you write the text matching yourselfYes, built in
Sound libraryNone included — you supply the files72+ sounds, plus MP3, WAV, and CAF uploads
Survives macOS updatesYour element paths may need re-checking each releaseKept working across releases as part of the app
Requires LuaYesNo
Checked against the Hammerspoon API documentation, August 2026. Hammerspoon can reach every row in this table; the column says what it costs to get there.

I ran this setup, and it broke

I built the Lua version before I built Chirpy. It worked, and then it didn't: an observer that quietly stopped firing, an element tree that moved under me after a macOS update, silence with nothing in the console to explain it. The failure mode is the problem. A notification sound that stops working without telling you is worse than no sound at all, because you go on trusting it — and you find out when you miss the message the whole setup existed for.

That's the reason Chirpy exists. Chirpy is a native Mac menu bar app that plays a chosen sound when a matching notification banner appears: the same Accessibility observer, hardened, plus a rules list covering the 118 apps in the notification guide directory, matching on app, channel, sender, or keyword with the most specific rule winning, a library of more than 72 sounds with per-rule volume, and the element paths re-checked on every macOS release so your sounds keep playing. It asks for Accessibility access and never Screen Recording, and notification text never leaves your Mac.

It's $19.99 once with no subscription, free to try for 3 days without a card, 30 days to ask for your money back, and it needs macOS 13 or later. Three years of releases have gone into the parts the snippet above gets wrong. See what the licence covers.

Keep Hammerspoon for what it's great at. Window management, hotkeys, and the automations you've already written are worth every line, and Chirpy sits alongside them without touching any of it. The one job worth handing over is the fragile one.

Hammerspoon is the strongest of the DIY options, which is why it gets the working code. The others are worth knowing about: whether Apple Shortcuts can change notification sounds (it can't — there's no notification trigger on macOS), the slack-sounds scripts on GitHub (a cache hack and a per-channel bot, both real, both narrow), and per-channel Slack notification sounds if Slack is the only app you care about. For the general version of the problem, start with setting a per-app notification sound on Mac. And the one-line summary of Hammerspoon's place in all this lives on its own page in the directory: Hammerspoon notification sounds.

Frequently asked questions

Yes, with code you write. An hs.axuielement.observer watching Notification Center fires when a banner appears, you read the banner's text, match the app or phrase you care about, and call hs.sound.getByFile(path):play(). Hammerspoon ships no such rule out of the box.

Not Slack's own sound. Mute Slack's audio, then have Hammerspoon play your file when a Slack banner appears. You get the sound you want; you also maintain the Lua, and Slack's own preferences stay untouched.

It observes the text already visible in Notification Center banners. Hammerspoon and Chirpy both use it for the same reason: it's the only supported way on macOS to know that another app's notification just appeared.

No. hs.notify sends notifications from Hammerspoon itself, and its soundName parameter picks the sound for those. It has no effect on notifications posted by Slack, Mail, or anything else on your Mac.

Comparisons reflect publicly available information as of August 2026.