A persistent macOS notification server that displays rich notifications with working action buttons, even when called from background processes.
Tools like terminal-notifier and alerter have issues on modern macOS (Sequoia/Tahoe) when called from background scripts (like git hooks):
- Action buttons don't work reliably
- Notifications may not appear
- Permission issues in background contexts
DandyNotifier runs as a persistent menu bar agent with proper notification permissions. Background scripts make HTTP requests to the server, which handles notifications in a privileged context where action buttons work correctly.
- ✅ Persistent background server (survives script termination)
- ✅ Working action buttons from background processes
- ✅ Rich notifications (title, subtitle, message, sound, icon)
- ✅ Notification grouping
- ✅ Custom sounds and icons
- ✅ Authenticated HTTP API
- ✅ Simple CLI tool
- ✅ Auto-starts on login (via LaunchAgent)
cd /path/to/DandyNotifier
xcodebuild -project DandyNotifier.xcodeproj -scheme DandyNotifier -configuration Releasecp -r build/Release/DandyNotifier.app /Applications/chmod +x CLI/dandy-notify
sudo cp CLI/dandy-notify /usr/local/bin/The LaunchAgent will start the app on login but won't auto-restart if you quit it.
cp LaunchAgent/com.orthly.DandyNotifier.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.orthly.DandyNotifier.plistNote: The app can be quit normally via the menu bar "Quit" option and will stay quit until you log in again or manually restart it. If you want the app to auto-restart even after quitting, edit the plist and add:
<key>KeepAlive</key>
<true/>Launch DandyNotifier.app from Applications. It will:
- Request notification permissions (if needed)
- Start HTTP server on port 8889
- Create auth token in
~/.dandy-notifier-token - Show menu bar icon (bell)
dandy-notify -t "Build Complete" -m "Your project compiled successfully"dandy-notify \
-t "Test Failed" \
-m "Click to view logs" \
-o "file:///tmp/test.log"#!/bin/bash
# .git/hooks/post-checkout
LOG_FILE="/tmp/post-checkout.log"
date > "$LOG_FILE"
if ! ./run-tests.sh >> "$LOG_FILE" 2>&1; then
dandy-notify \
-t "Repository" \
-s "post-checkout hook" \
-m "Tests failed. Click to view logs." \
-o "file://$LOG_FILE" \
-g "git-hooks" \
--sound "/System/Library/Sounds/Basso.aiff"
ficurl http://localhost:8889/healthTOKEN=$(cat ~/.dandy-notifier-token)
curl -X POST http://localhost:8889/notify \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"notification": {
"title": "Hello",
"message": "World",
"subtitle": "Test",
"group": "myapp",
"sound": "/System/Library/Sounds/Ping.aiff",
"action": {
"id": "open_logs",
"label": "Show Logs",
"type": "open",
"location": "file:///tmp/logs.txt"
}
}
}'Headers:
Content-Type: application/jsonAuthorization: Bearer <token>
Body:
{
"notification": {
"title": "string (required)",
"message": "string (required)",
"subtitle": "string (optional)",
"group": "string (optional)",
"sound": "string (optional, path to .aiff file)",
"action": {
"id": "string (required)",
"label": "string (required)",
"type": "string ('open' or 'execute')",
"location": "string (required, file:// URL, path, or shell command)"
}
}
}Response:
200 OK- Notification sent400 Bad Request- Invalid JSON401 Unauthorized- Invalid/missing token500 Internal Server Error- Server error
Returns 200 OK if server is running.
┌─────────────────────────────────────────┐
│ DandyNotifier.app (Menu Bar Agent) │
│ ┌─────────────────────────────────┐ │
│ │ HTTP Server (Port 8889) │ │
│ │ - POST /notify │ │
│ │ - GET /health │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ NotificationManager │ │
│ │ - UNUserNotificationCenter │ │
│ │ - Action button handling │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Menu Bar UI │ │
│ │ - Bell icon │ │
│ │ - Quit option │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
▲
│ HTTP POST (localhost only)
│
┌──────────┴──────────┐
│ CLI Tool │
│ dandy-notify │
│ (or direct curl) │
└─────────────────────┘
- Server only accepts connections from localhost
- Authentication token required for all requests
- Token stored with restrictive permissions (0600)
- Non-sandboxed app (required for arbitrary file access)
- Check System Settings > Notifications > DandyNotifier
- Ensure "Allow Notifications" is enabled
- IMPORTANT: Set alert style to "Alerts" (not "Banners")
- This makes notifications persist until dismissed
- Action buttons appear as prominent bordered sections on the right
- Without this, action buttons only show on hover and notifications auto-dismiss
- Make sure you're using
file://URLs for local files - Check that DandyNotifier.app is running
- Verify permissions in System Settings
- Check if port 8889 is already in use:
lsof -i :8889 - Look at Console.app for DandyNotifier logs
- Launch DandyNotifier.app manually once
- Token is created at
~/.dandy-notifier-token
If the app won't quit or keeps restarting:
- Check if LaunchAgent has
KeepAlive = true(it will auto-restart) - Unload the LaunchAgent:
launchctl unload ~/Library/LaunchAgents/com.orthly.DandyNotifier.plist - Then quit the app from the menu bar
- By default, the LaunchAgent does NOT use KeepAlive, so quit should work normally
- Ensure
ENABLE_APP_SANDBOX = NOin Xcode build settings - Check entitlements file has
<key>com.apple.security.app-sandbox</key><false/> - Rebuild after changing these settings
| Feature | terminal-notifier | DandyNotifier |
|---|---|---|
| Background process support | ❌ Actions broken | ✅ Works perfectly |
| Sequoia compatibility | ✅ Full | |
| Action buttons | ❌ From background | ✅ Always works |
| Persistent server | ❌ | ✅ |
| HTTP API | ❌ | ✅ |
| Custom sounds | ✅ | ✅ |
| Custom icons | ✅ | ✅ |
| Grouping | ✅ | ✅ |
MIT
Created by James Zetlen for Dandy