docs/skills/smart-ntfy/SKILL.md
Send intelligent notifications via ~/bin/ntfy with context-aware channel selection. Use when completing tasks, asking questions, encountering errors, or reaching milestones.
npx skillsauth add megalithic/dotfiles smart-ntfyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
# 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
ntfy <command> [options]
Commands:
send Send a notification
answer Mark a question as answered
pending List pending questions
help Show help message
| Short | Long | Description |
|-------|--------------|---------------------------------------------|
| -t | --title | Notification title (required) |
| -m | --message | Notification message (required) |
| -u | --urgency | normal|high|critical (default: normal) |
| -s | --source | Source app name (auto-detected if omitted) |
| -S | --no-source| Disable source prefix in title |
| -p | --phone | Send to phone via iMessage |
| -P | --pushover | Send via Pushover |
| -q | --question | Track for retry if unanswered |
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"
The ntfy script automatically routes based on user attention:
Canvas Notification - On-screen overlay (HAL 9000 icon)
macOS Notification Center - Always sent for logging
~/.local/share/hammerspoon/hammerspoon.dbPushover - Remote phone notification
critical urgency-PiMessage - Direct to user's phone
critical urgency-pShould 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
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
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
| Situation | Urgency | Why |
|-----------|---------|-----|
| Task completed successfully | normal | User will see canvas |
| Task completed with warnings | high | Draw more attention |
| Task failed/error | critical | Sends to phone too |
| Question needing answer | high | Centered, prominent |
| Security vulnerability found | critical | Always notify phone |
| Long task progress update | normal | Non-intrusive |
DO send for:
DON'T send for:
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
The ntfy script automatically detects if you're paying attention:
If user IS paying attention → subtle NC notification only If user NOT paying attention → canvas overlay + NC + optional remote
# 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
The ntfy script delegates all logic to Hammerspoon's notification system:
ntfy send → N.send(opts) → routeNotification() → sendCanvas/sendMacOS/sendPhone
↓
sendCanvasNotification()
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"
paying_attention → subtle | not_paying_attention → full | display_asleep → remote_only~/bin/ntfy - CLI wrapper (bash)~/.dotfiles/config/hammerspoon/lib/notifications/send.lua - N.send() API~/.dotfiles/config/hammerspoon/lib/notifications/notifier.lua - Canvas rendering~/.dotfiles/config/hammerspoon/lib/notifications/processor.lua - Rule processing~/.dotfiles/config/hammerspoon/watchers/notification.lua - NC capture~/.local/share/hammerspoon/hammerspoon.db - Notification historyThe send command returns a space-separated status line:
<status> <reason> <channels> [questionId]
| Field | Values | Description |
|-------|--------|-------------|
| status | sent / suppressed | Whether notification was sent |
| reason | paying_attention / not_paying_attention / display_asleep | Why routing decision was made |
| channels | macos,canvas,phone | Comma-separated list of channels used |
| questionId | 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
| 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) |
# 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
# 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
# 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
# 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')"
# List pending questions
ntfy pending
# Check question database
hs -c "
local N = require('lib.notifications')
local pending = N.getPendingQuestions()
print(#pending .. ' pending questions')
"
# 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"
# 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
# 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"
testing
Apply Strunk's timeless writing rules to ANY prose humans will read - documentation, commit messages, error messages, explanations, reports, or UI text. Makes your writing clearer, stronger, and more professional.
tools
Web search using DuckDuckGo (free, unlimited). Falls back to pi-web-access extension for content extraction.
tools
Interact with web pages using agent-browser CLI. MUST run 'browser connect 9222' FIRST to use existing browser with authenticated sessions.
tools
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output.