Dotfiles smart-ntfy
Send intelligent notifications via ~/bin/ntfy with context-aware channel selection. Use when completing tasks, asking questions, encountering errors, or reaching milestones.
git clone https://github.com/megalithic/dotfiles
T=$(mktemp -d) && git clone --depth=1 https://github.com/megalithic/dotfiles "$T" && mkdir -p ~/.claude/skills && cp -r "$T/docs/skills/smart-ntfy" ~/.claude/skills/megalithic-dotfiles-smart-ntfy && rm -rf "$T"
docs/skills/smart-ntfy/SKILL.mdSmart Notification System (ntfy)
Overview
You have access to a sophisticated multi-channel notification system via
~/bin/ntfy. This skill helps you make smart decisions about when and how to notify the user.
Quick Reference
# Basic notification ntfy send -t "Title" -m "Message" # With urgency levels: normal|high|critical ntfy send -t "Title" -m "Message" -u high # Send to phone via Pushover (for remote notifications) ntfy send -t "Title" -m "Message" -P # Question that may need retry (tracks until answered) ntfy send -t "Question" -m "Should I continue?" -q # Mark a question as answered ntfy answer -t "Question" -m "Should I continue?" # List pending unanswered questions ntfy pending
Command Structure
ntfy <command> [options] Commands: send Send a notification answer Mark a question as answered pending List pending questions help Show help message
Options
| Short | Long | Description |
|---|---|---|
| | Notification title (required) |
| | Notification message (required) |
| | normal|high|critical (default: normal) |
| | Source app name (auto-detected if omitted) |
| | Disable source prefix in title |
| | Send to phone via iMessage |
| | Send via Pushover |
| | Track for retry if unanswered |
Source Detection
By default, ntfy auto-detects the calling program by walking up the process tree and prefixes the title (e.g.,
[claude] Task Done). This helps identify which
tool sent the notification.
# Auto-detection (default) - title becomes "[claude] Done" if called from Claude ntfy send -t "Done" -m "Tests passed" # Disable source prefix entirely ntfy send -t "Done" -m "Tests passed" -S # Override with custom source name ntfy send -t "Done" -m "Tests passed" -s "myapp" # → "[myapp] Done"
Notification Channels
The ntfy script automatically routes based on user attention:
-
Canvas Notification - On-screen overlay (HAL 9000 icon)
- Normal: Bottom-left, 5 seconds
- High/Critical: Center screen with dimmed background
-
macOS Notification Center - Always sent for logging
- Captured by Hammerspoon watcher
- Logged to SQLite:
~/.local/share/hammerspoon/hammerspoon.db
-
Pushover - Remote phone notification
- Auto-sent on
urgencycritical - Or explicitly with
-P
- Auto-sent on
-
iMessage - Direct to user's phone
- Auto-sent on
urgencycritical - Or explicitly with
-p
- Auto-sent on
Decision Trees
"Should I send a notification?"
Should I notify? │ ├─▶ Task completed after 30+ seconds? │ └─▶ YES → Send normal urgency │ ├─▶ Error/failure occurred? │ └─▶ Recoverable error → high urgency │ └─▶ Critical error → critical urgency (sends to phone) │ ├─▶ Need user input/decision? │ └─▶ YES → Send with -q flag (question tracking) │ └─▶ Set urgency to high for prominence │ ├─▶ Security issue found? │ └─▶ ALWAYS send critical (phone notification) │ ├─▶ Progress update on long task? │ └─▶ Only at milestones, normal urgency │ └─▶ Minor step completed? └─▶ DON'T send - too noisy
"What urgency should I use?"
Urgency selection? │ ├─▶ User MUST see this NOW (security, critical failure)? │ └─▶ critical (auto-sends to phone) │ ├─▶ User should see soon but not life-threatening? │ └─▶ high (centered overlay, longer duration) │ ├─▶ FYI / completed task / progress? │ └─▶ normal (corner overlay, 5 seconds) │ └─▶ User is actively watching this terminal? └─▶ Consider not sending at all
"Should I send to phone?"
Phone notification? │ ├─▶ Urgency is critical? │ └─▶ Auto-sent to phone (you don't need to add -p or -P) │ ├─▶ User explicitly requested remote notification? │ └─▶ Use -P (Pushover) or -p (iMessage) │ ├─▶ User is away from desk (display asleep)? │ └─▶ Auto-routed to phone for critical │ └─▶ Normal/high → phone only if explicitly requested │ └─▶ User is at desk? └─▶ Canvas overlay is sufficient
Urgency Guidelines
| Situation | Urgency | Why |
|---|---|---|
| Task completed successfully | | User will see canvas |
| Task completed with warnings | | Draw more attention |
| Task failed/error | | Sends to phone too |
| Question needing answer | | Centered, prominent |
| Security vulnerability found | | Always notify phone |
| Long task progress update | | Non-intrusive |
When to Send Notifications
DO send for:
- Task completion (especially long-running)
- Errors requiring user attention
- Questions needing user input
- Significant milestones
- Security findings
DON'T send for:
- Minor steps completed
- Info user is actively watching
- Debugging output
- Redundant status updates
Message Best Practices
- Titles: Keep under 50 characters, be specific
- Messages: Keep under 200 characters, include key details
- Include metrics: "42 tests passed in 3.2s" not just "Tests passed"
- Be actionable: "Check logs at /tmp/build.log" not just "Error occurred"
Question Tracking
The
-q flag marks a notification as a question that needs acknowledgment:
# Send a question ntfy send -t "Confirm" -m "Deploy to production?" -u high -q # Later, mark it as answered ntfy answer -t "Confirm" -m "Deploy to production?" # Or answer by ID (returned from send) ntfy answer -i <question_id> # Check pending questions ntfy pending
Attention Detection
The ntfy script automatically detects if you're paying attention:
- Checks if terminal app is frontmost
- Checks current tmux session/window
- Checks display state (asleep/locked)
If user IS paying attention → subtle NC notification only If user NOT paying attention → canvas overlay + NC + optional remote
Examples
# Task completed ntfy send -t "Build Complete" -m "42 tests passed, 0 failures in 3.2s" # Error with high urgency ntfy send -t "Build Failed" -m "3 type errors in src/auth.ts:45,78,123" -u high # Critical security finding (auto-sends to phone) ntfy send -t "Security Alert" -m "Found hardcoded API key in config.js" -u critical # Question for user ntfy send -t "Clarification Needed" -m "Should I refactor the auth module or just fix the bug?" -u high -q # Send to phone when away ntfy send -t "Task Done" -m "Deployment completed successfully" -p
Internal Architecture
The ntfy script delegates all logic to Hammerspoon's notification system:
ntfy send → N.send(opts) → routeNotification() → sendCanvas/sendMacOS/sendPhone ↓ sendCanvasNotification()
Key Function Signatures
N.send() - Main entry point (lib/notifications/send.lua)
N.send({ title = "string", -- Required message = "string", -- Required urgency = "normal", -- "normal"|"high"|"critical" phone = false, -- Send via iMessage pushover = false, -- Send via Pushover question = false, -- Track for retry context = "session:window:pane", -- tmux context for attention detection }) -- Returns: { sent = bool, channels = {"macos","phone"}, reason = string, questionId = string|nil }
sendCanvasNotification() - Visual overlay (lib/notifications/notifier.lua)
sendCanvasNotification(title, message, opts) -- opts: { subtitle?, duration?, anchor?, position?, dimBackground?, appImageID?, appBundleID?, includeProgram?, ... } -- Uses U.defaults() for merging - subtitle defaults to "", duration to config.defaultDuration or 5
M.process() - Rule-based routing (lib/notifications/processor.lua)
M.process(rule, opts) -- opts: { title, subtitle?, message, axStackingID, bundleID, notificationID?, notificationType?, subrole?, matchedCriteria?, urgency? } -- Uses U.defaults() for merging - title/subtitle/message default to "", urgency to "normal"
Attention Detection Flow
- Check display state (awake/asleep/locked)
- Check if terminal is frontmost app
- Query tmux for active session:window:pane
- Compare against calling context
- Route:
→ subtle |paying_attention
→ full |not_paying_attention
→ remote_onlydisplay_asleep
Related Files
- CLI wrapper (bash)~/bin/ntfy
- N.send() API~/.dotfiles/config/hammerspoon/lib/notifications/send.lua
- Canvas rendering~/.dotfiles/config/hammerspoon/lib/notifications/notifier.lua
- Rule processing~/.dotfiles/config/hammerspoon/lib/notifications/processor.lua
- NC capture~/.dotfiles/config/hammerspoon/watchers/notification.lua
- Notification history~/.local/share/hammerspoon/hammerspoon.db
Return Values and Error Handling
ntfy send Output
The send command returns a space-separated status line:
<status> <reason> <channels> [questionId]
| Field | Values | Description |
|---|---|---|
| / | Whether notification was sent |
| / / | Why routing decision was made |
| | Comma-separated list of channels used |
| UUID or empty | ID for tracking questions |
Example outputs:
sent not_paying_attention canvas,macos # User away, canvas shown sent paying_attention macos # User watching, subtle NC only sent display_asleep phone,macos # Screen locked, sent to phone suppressed paying_attention # User watching, suppressed
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success (notification sent or suppressed as intended) |
| 1 | Invalid arguments (missing title/message) |
| 1 | Unknown command |
| Non-zero | Hammerspoon error (hs command failed) |
Error Handling Patterns
# Check if notification was sent result=$(ntfy send -t "Done" -m "Task complete") if [[ "$result" == sent* ]]; then echo "Notification delivered" fi # Check which channels were used result=$(ntfy send -t "Done" -m "Task complete" -u critical) if [[ "$result" == *"phone"* ]]; then echo "Sent to phone" fi # Get question ID for later answering result=$(ntfy send -t "Question" -m "Continue?" -q) question_id=$(echo "$result" | awk '{print $4}') if [[ -n "$question_id" ]]; then # Store for later: ntfy answer -i "$question_id" echo "Question ID: $question_id" fi
Troubleshooting
Notifications Not Appearing
# 1. Check Hammerspoon is running pgrep Hammerspoon || echo "Hammerspoon not running!" # 2. Check hs CLI works hs -c "print('hello')" # Should print: hello # 3. Check N module loads hs -c "local N = require('lib.notifications'); print('loaded')" # Should print: loaded # 4. Verify macOS permissions # System Settings → Notifications → Hammerspoon → Allow # 5. Check for Lua errors hs -c "local N = require('lib.notifications'); N.send({title='test', message='test'})" # Should return status line, not error
Canvas Not Showing
# Check canvas availability hs -c "print(hs.canvas)" # Should print: table: 0x... # Check screen count hs -c "print(#hs.screen.allScreens())" # Should be > 0 # Force canvas test hs -c " local c = hs.canvas.new({x=100,y=100,w=200,h=100}) c:appendElements({type='rectangle', fillColor={red=1}}) c:show() hs.timer.doAfter(2, function() c:delete() end) " # Red rectangle should appear for 2 seconds
Phone Notifications Not Working
# Check Pushover credentials hs -c "print(N.config.pushover.userKey and 'configured' or 'missing')" # Check iMessage can send hs -c "print(N.config.phoneNumber or 'no phone number')" # Test iMessage directly (be careful, actually sends!) # hs -c "hs.messages.iMessage('phone_number', 'test')"
Question Tracking Issues
# List pending questions ntfy pending # Check question database hs -c " local N = require('lib.notifications') local pending = N.getPendingQuestions() print(#pending .. ' pending questions') "
Self-Discovery Patterns
Exploring the API
# Show help ntfy help # Show N module functions hs -c "for k,v in pairs(require('lib.notifications')) do print(k, type(v)) end" # Show config values hs -c "local N = require('lib.notifications'); for k,v in pairs(N.config or {}) do print(k,v) end"
Testing Different Urgencies
# Test normal (corner, 5s) ntfy send -t "Test" -m "Normal urgency" -u normal # Test high (centered, longer) ntfy send -t "Test" -m "High urgency" -u high # Test critical (centered + phone) # Warning: Actually sends to phone! # ntfy send -t "Test" -m "Critical urgency" -u critical
Checking Attention State
# Check if terminal is focused hs -c "print(hs.window.focusedWindow():application():name())" # Check display state hs -c "print(hs.caffeinate.get('displayIdle') and 'active' or 'idle')" # Check current tmux context tmux display-message -p '#S:#I:#P' 2>/dev/null || echo "not in tmux"
Known Limitations
- iMessage requires permissions - System must have access to Messages
- Pushover requires API key - Must be configured in Hammerspoon
- Canvas requires Accessibility - Must grant Hammerspoon accessibility access
- Source detection heuristic - May not always identify the correct calling app
- Question tracking in memory - Questions lost on Hammerspoon restart