Set up webhook-to-desktop notification systems using Cloudflare Workers and ntfy.sh...
Set up secure, reliable webhook-to-desktop notification systems that receive webhooks from any source and display native desktop notifications on macOS.
The system uses a proven pattern:
Benefits:
Run the init script to set up a new webhook notification system:
scripts/init_webhook.sh <webhook-name> <ntfy-topic> [output-dir] [sound-file]
Example:
scripts/init_webhook.sh stripe-payments myapp-stripe
This creates:
Run init_webhook.sh with appropriate parameters:
Arguments:
webhook-name: Identifier for this webhook (e.g., "stripe-payments", "github-prs")ntfy-topic: Your unique ntfy.sh topic name (e.g., "myapp-notifications-2024")output-dir: (Optional) Where to create files (default: ~/.claude/webhooks/<webhook-name>)sound-file: (Optional) Custom notification sound (default: system sound)Example with custom sound:
scripts/init_webhook.sh github-alerts my-github-topic ~/webhooks /path/to/alert.mp3
Edit the generated worker.js to handle your specific webhook format.
The worker template includes examples for:
Modify the extractNotification() function to parse your webhook structure and return notification details.
Example for custom app webhooks:
function extractNotification(event) {
// Your custom webhook format
if (event.type === 'user.signup') {
return {
title: 'New User Signup',
message: `${event.user_email} just created an account`,
priority: '4',
tags: 'user,tada',
url: `https://myapp.com/admin/users/${event.user_id}`,
};
}
return null; // Ignore other events
}
Notification object fields:
title: Notification title (required)message: Notification body text (required)priority: 1-5, where 5 is highest (optional, default: 3)tags: Emoji tags for ntfy.sh like "moneybag", "warning", "fire" (optional)url: URL to open when notification is clicked (optional)For common patterns, see EXAMPLES.md with complete examples for:
Navigate to the webhook directory and deploy:
cd ~/.claude/webhooks/<webhook-name>/webhook
npx wrangler deploy
The deployment will output your worker URL:
https://<webhook-name>.<your-subdomain>.workers.dev
Configure your webhook source to send POST requests to:
https://<webhook-name>.<your-subdomain>.workers.dev/webhook
Optional: Add webhook secret for signature verification:
npx wrangler secret put WEBHOOK_SECRET
Install the listener as a background service that starts automatically:
scripts/install_service.sh ~/.claude/webhooks/<webhook-name>/com.webhook.<webhook-name>.plist
The service will:
Test manually before installing:
~/.claude/webhooks/<webhook-name>/listener.sh
Then send a test notification:
curl -d 'Test notification!' https://ntfy.sh/<your-topic>
Send a test webhook to verify the complete flow:
curl -X POST https://<webhook-name>.<your-subdomain>.workers.dev/webhook \
-H "Content-Type: application/json" \
-d '{"title":"Test","message":"Hello from webhook!"}'
You should see:
~/.claude/webhooks/<webhook-name>/logs/listener.logNo notification received:
cd webhook && npx wrangler tailtail -f ~/.claude/webhooks/<webhook-name>/logs/*.loglaunchctl list | grep webhookcurl -d "Test" https://ntfy.sh/<your-topic>Notification sound not playing:
terminal-notifierService keeps stopping:
cat ~/.claude/webhooks/<webhook-name>/logs/stderr.logterminal-notifier is installed: brew install terminal-notifierToo many notifications (keepalive messages):
extractNotification() logicCreate separate notification systems for different sources:
scripts/init_webhook.sh stripe-payments myapp-stripe
scripts/init_webhook.sh github-alerts myapp-github
scripts/init_webhook.sh error-monitoring myapp-errors
Each runs independently with its own worker, listener, and service.
Return null from extractNotification() to ignore events:
function extractNotification(event) {
// Ignore test events
if (event.test || event.environment !== 'production') {
return null;
}
// Ignore low-priority actions
if (event.action === 'labeled') {
return null;
}
// Handle other events...
}
Modify listener.sh to play different sounds by priority:
priority=$(echo "$message" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('priority','3'))" 2>/dev/null || echo "3")
if [[ "$priority" == "5" ]]; then
afplay "/System/Library/Sounds/Sosumi.aiff" & # Critical
elif [[ "$priority" == "4" ]]; then
afplay "$CUSTOM_SOUND" & # High priority
fi
ntfy.sh supports image attachments and action buttons:
await fetch(NTFY_URL, {
method: 'POST',
headers: {
'Title': notification.title,
'Attach': 'https://example.com/screenshot.png',
'Actions': 'view, View details, https://example.com',
},
body: notification.message,
});
Check if service is running:
launchctl list | grep com.webhook.<webhook-name>
View live logs:
tail -f ~/.claude/webhooks/<webhook-name>/logs/*.log
Restart service:
launchctl unload ~/Library/LaunchAgents/com.webhook.<webhook-name>.plist
launchctl load ~/Library/LaunchAgents/com.webhook.<webhook-name>.plist
Stop service:
launchctl unload ~/Library/LaunchAgents/com.webhook.<webhook-name>.plist
Uninstall completely:
launchctl unload ~/Library/LaunchAgents/com.webhook.<webhook-name>.plist
rm ~/Library/LaunchAgents/com.webhook.<webhook-name>.plist
rm -rf ~/.claude/webhooks/<webhook-name>
init_webhook.sh - Initialize new webhook notification systeminstall_service.sh - Install listener as macOS background serviceworker-template.js - Cloudflare Worker templatelistener-template.sh - Local listener script templatelaunchd-template.plist - macOS service configuration templateEXAMPLES.md - Complete examples for common webhook sources (Stripe, GitHub, error monitoring, custom apps) with code samples and patterns