notify is a macOS command-line tool for sending and managing native notifications. Designed for AI agents running on macOS, it lets agents keep the user informed, ask for decisions via interactive action buttons, and report results — all through native macOS notifications.
- AI agents can notify you during long-running tasks without cluttering the terminal
- Interactive action buttons let agents ask for confirmation (approve/retry/rollback) — and wait for your response
- Scriptable JSON output so agents can parse results and errors programmatically
- Persistent local store for auditing past notifications and user actions
- LaunchAgent support for background action listening across sessions
- macOS 13+
- Swift 6+ (only required for build-from-source)
brew tap xBirahim/notify
brew install xbirahim/notify/notify
notify request-permission --soundTo upgrade: brew upgrade notify
git clone https://github.com/xBirahim/notify.git
cd notify
make installThis installs:
~/.local/share/Notify.app(required app bundle identity forUNUserNotificationCenter)~/.local/bin/notify(launcher)
Add the launcher to your shell path:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
notify versionswift build -c release
cp .build/release/notify /usr/local/bin/Direct binary execution can fail permission flows because macOS notification APIs expect an app bundle identity. Use Homebrew or make install for reliable behavior.
# 1) Check current notification permission state
notify status
# 2) Request permission (opens System Settings when needed)
notify request-permission --sound
# 3) Send your first notification
notify send "Hello from notify"
# 4) View stored notifications
notify listThe root SKILL.md teaches AI agents to:
- Send progress notifications during long tasks
- Ask for confirmation using interactive action buttons
- Report results with status and timing
- Handle permission setup automatically
- Parse JSON output for decision-making
Load SKILL.md into your AI agent's context, or configure your tool to include it automatically (OpenCode: add to skills.paths or instructions; Cursor: add to .cursorrules).
notify
├── status
├── request-permission
├── send
├── update
├── dismiss
├── list
├── get
├── test
├── listen
├── agent
│ ├── install
│ ├── uninstall
│ └── status
└── version
Most commands support these flags:
--json: emit machine-readable JSON--quiet: suppress non-error output--dry-run: validate inputs without side effects
notify version --json returns a direct JSON object (not the standard command envelope).
Most --json command responses use this shape:
{
"id": "deploy-api-prod",
"command": "send",
"status": "delivered",
"data": {},
"error": null
}Error responses:
{
"id": "deploy-api-prod",
"command": "get",
"status": "error",
"data": null,
"error": {
"code": "not_found",
"message": "Notification not found.",
"detail": null
}
}Show current macOS notification authorization state.
notify status
notify status --jsonRequest notification permissions.
Options:
--sound--provisional--critical--json--quiet
Examples:
notify request-permission
notify request-permission --sound --json
notify request-permission --provisional --jsonIf command-line permission prompting fails, notify opens System Settings and relaunches itself to complete authorization.
Send a notification.
Arguments:
- positional
body(optional if--bodyis provided)
Options:
--id <id>--title <title>--subtitle <subtitle>--body <body>--category <plain|alert|job|deploy>--thread <thread>--url <url>--sound <default\|none\|name>--interruption-level <passive|active>--user-info key=value(repeatable)--json--quiet--dry-run
Examples:
# Minimal
notify send "Build succeeded"
# Full payload
notify send \
--id deploy-api-prod \
--title "Operation in progress" \
--subtitle "api-service / production" \
--body "Step 2/5: running migrations" \
--category deploy \
--thread deploy-api-prod \
--url "https://grafana.example.com/d/deploy" \
--sound default \
--interruption-level active \
--user-info env=prod \
--user-info service=api \
--json
# Validate only
notify send "Will not be sent" --dry-run --jsonReplace a notification using an existing ID.
Arguments:
id(required)
Options:
--title--subtitle--body--category <plain|alert|job|deploy>--thread <thread>--url <url>--sound <default\|none\|name>--interruption-level <passive|active>--user-info key=value(repeatable)--json--quiet--dry-run
Examples:
notify update deploy-api-prod \
--title "Operation complete" \
--body "Completed in 3m42s" \
--sound default
notify update deploy-api-prod --body "Rollback started" --category deploy --jsonDismiss notifications by ID, by thread, or all.
Target options (mutually exclusive):
- positional
idor--id <id> --thread <thread>--all
Scope options:
--pending--delivered
If no scope is provided, both pending and delivered notifications are dismissed.
Examples:
notify dismiss deploy-api-prod
notify dismiss --thread deploy-api-prod
notify dismiss --all
notify dismiss --all --pending
notify dismiss --thread incident-payments --delivered --jsonList notifications from the local store.
Options:
--thread <thread>--json--quiet
Examples:
notify list
notify list --thread deploy-api-prod
notify list --jsonRetrieve one notification by ID from the local store.
notify get deploy-api-prod
notify get deploy-api-prod --jsonSend a standard test notification (id=notify-test).
notify test
notify test --interruption-level active --json
notify test --dry-runRun a foreground listener for notification action events. This process stays alive until SIGINT/SIGTERM.
notify listenEach action event is emitted to stdout as JSON (JSON Lines), for example:
{"action":"OPEN","category":"deploy","notification_id":"deploy-api-prod","timestamp":"2026-05-28T19:12:22Z","url":"https://grafana.example.com/d/deploy"}Manage the background LaunchAgent that runs notify listen.
notify agent install
notify agent status
notify agent uninstallPaths used by the agent:
- plist:
~/Library/LaunchAgents/io.notify.listener.plist - log:
~/Library/Logs/notify/listener.log
notify agent status reports service state as one of running, loaded, or not loaded.
Print installed version information.
notify version
notify version --jsonnotify supports macOS system sounds and custom sound files. Pass the sound name (without extension) via --sound:
notify send "Hello" --sound Purr
notify send "Silent" --sound none| Name | Description |
|---|---|
Basso |
Deep bass |
Blow |
Air blow |
Bottle |
Glass bottle |
Frog |
Frog croak |
Funk |
Funky riff |
Glass |
Glass breaking |
Hero |
Heroic fanfare |
Morse |
Morse code |
Ping |
Sonar ping |
Pop |
Pop cork |
Purr |
Cat purr |
Sosumi |
Classic macOS chime |
Submarine |
Submarine sonar |
Tink |
Small bell |
default |
System default notification sound |
none |
No sound |
Sound files are loaded from /System/Library/Sounds/ and ~/Library/Sounds/. Supported formats: .aiff, .caf, .wav, .mp3, .m4a.
# Custom sound from ~/Library/Sounds/
notify send "Custom alert" --sound MyAlert.caf
# Combine with interruption level for urgent alerts
notify send "Critical failure" --sound Sosumi --interruption-level activenotify registers these categories:
| Category | Buttons | Use case |
|---|---|---|
plain |
(none) | Pure information, no interaction |
alert |
ACK, OPEN, SILENCE | Alert that needs acknowledgment |
job |
ACK, RETRY, OPEN | Background job the user can retry |
deploy |
OPEN, ROLLBACK, ACK | Operation to approve or rollback |
When user clicks the notification body (default action) or the OPEN button, notify opens the attached --url if provided.
notify persists records as JSON Lines:
~/Library/Application Support/notify/notifications.jsonl~/Library/Application Support/notify/actions.jsonl
notify list returns the latest stored record per notification ID (deduplicated from append-only history).
Practical queries:
# Latest 20 action events
tail -n 20 "$HOME/Library/Application Support/notify/actions.jsonl"
# Find all deploy actions
jq -c 'select(.category == "deploy")' "$HOME/Library/Application Support/notify/actions.jsonl"
# List all notifications for a thread
notify list --thread deploy-api-prod --json | jq '.data[] | {id, title, body, updated_at}'
# Count retry actions
jq -r 'select(.action == "RETRY") | .notification_id' \
"$HOME/Library/Application Support/notify/actions.jsonl" | wc -lSend progressive updates so the user knows what your agent is doing.
TASK_ID="build-site-$(date +%s)"
notify send \
--id "$TASK_ID" \
--title "Building site" \
--body "Step 1/4: fetching content..." \
--category plain \
--thread "$TASK_ID" \
--json
# ... do work ...
notify update "$TASK_ID" --body "Step 2/4: generating pages..." --json
# ... do work ...
notify update "$TASK_ID" --body "Step 3/4: optimizing assets..." --json
# Final update
notify update "$TASK_ID" \
--title "Build complete" \
--body "Generated 342 pages in 1.2s" \
--sound default \
--jsonUse action categories — deploy, job, or alert — to attach interactive buttons, then run notify listen to capture the user's choice.
# Ask user to confirm
notify send \
--id cleanup-db \
--title "Cleanup old records?" \
--body "Delete 1,204 stale records from the database? This cannot be undone." \
--category job \
--thread cleanup \
--json
# Wait up to 2 minutes for response
RESPONSE=$(timeout 120 notify listen 2>/dev/null | head -n 1 | jq -r '.action // "timeout"')
if [ "$RESPONSE" = "ACK" ] || [ "$RESPONSE" = "OPEN" ]; then
# User approved — proceed
notify update cleanup-db --title "Cleaning up..." --body "Removing 1,204 records..." --json
# ... perform operation ...
notify update cleanup-db --title "Cleanup complete" --body "Removed 1,204 records in 3.2s" --sound default --json
elif [ "$RESPONSE" = "RETRY" ]; then
notify update cleanup-db --body "User requested retry — re-evaluating" --json
elif [ "$RESPONSE" = "timeout" ]; then
notify update cleanup-db --title "Operation skipped" --body "No response received — cancelled" --json
fiAfter completing a task, replace the progress notification with a clear result.
TASK_ID="backup-db-$(date +%s)"
notify send \
--id "$TASK_ID" \
--title "Starting backup" \
--body "Backing up production database..." \
--category plain \
--thread "$TASK_ID" \
--json
# ... perform backup ...
BACKUP_SIZE="2.4 GB"
DURATION="47s"
notify update "$TASK_ID" \
--title "Backup complete" \
--body "Database backed up successfully (${BACKUP_SIZE}, ${DURATION})" \
--sound default \
--jsonOn failure:
notify update "$TASK_ID" \
--title "Backup failed" \
--body "Database backup failed after ${DURATION}" \
--interruption-level active \
--jsonAUTH=$(notify status --json | jq -r '.data.authorization')
if [ "$AUTH" = "denied" ] || [ "$AUTH" = "notDetermined" ]; then
echo "Requesting notification permission"
notify request-permission --sound
finotify dismiss --thread maintenance-db-cluster --delivered --json
notify list --thread maintenance-db-cluster --json0: success44: not found64: usage65: invalid input69: permission denied70: system error124: timeout130: interrupted
swift build
swift test
swift build -c release
make installnotifycan only manage notifications owned by its own app identity.sendandupdateboth useUNNotificationRequest; same--idreplaces existing notification content.- Notifications and interaction callbacks require an active macOS user session.
- For reliable action capture across sessions, use
notify agent install. - When used by AI agents, prefer
--jsonoutput for reliable error checking. notify listentimes out automatically; for scripted agent workflows, wrap it withtimeoutto avoid indefinite hangs.