Skip to content

Instantly share code, notes, and snippets.

@UlisseMini
Last active January 6, 2026 00:30
Show Gist options
  • Select an option

  • Save UlisseMini/6f7100bb45c0f7b8becc6a44e6121cff to your computer and use it in GitHub Desktop.

Select an option

Save UlisseMini/6f7100bb45c0f7b8becc6a44e6121cff to your computer and use it in GitHub Desktop.
notifdump (get and follow macos notifications via cli)
#!/usr/bin/env bash
# IMPORTANT: Make sure your terminal has full-disk permissions or this won't work.
# ABOUTME: Dumps macOS notification records from usernoted database
# ABOUTME: Supports text, one-line, JSONL output, and follow mode
set -euo pipefail
usage() {
cat <<'EOF'
Usage: notifdump [-n COUNT] [-a APP_ID] [-o] [-j] [-f] [-v] [-h]
Options:
-n COUNT Records to read (default: 10)
-a APP_ID Filter by app identifier
-o One-line output
-j JSONL output (one JSON object per line)
-f Follow mode (watch for new notifications, requires entr)
-v Verbose (full plist dump)
-h Help
EOF
}
COUNT=10 APP_FILTER="" ONELINE=0 JSONL=0 VERBOSE=0 FOLLOW=0
while getopts ":n:a:ojfvh" opt; do
case "$opt" in
n) COUNT="$OPTARG" ;; a) APP_FILTER="$OPTARG" ;;
o) ONELINE=1 ;; j) JSONL=1 ;; f) FOLLOW=1 ;; v) VERBOSE=1 ;;
h) usage; exit 0 ;;
:) echo "Missing argument for -$OPTARG" >&2; exit 2 ;;
\?) echo "Unknown option: -$OPTARG" >&2; exit 2 ;;
esac
done
for cmd in sqlite3 plutil; do command -v "$cmd" >/dev/null || { echo "Missing: $cmd" >&2; exit 1; }; done
(( JSONL )) && { command -v jq >/dev/null || { echo "Missing: jq" >&2; exit 1; }; }
(( FOLLOW )) && { command -v entr >/dev/null || { echo "Missing: entr (brew install entr)" >&2; exit 1; }; }
DB="$HOME/Library/Group Containers/group.com.apple.usernoted/db2/db"
TMPDIR="$(mktemp -d)"; trap 'rm -rf "$TMPDIR"; exit 0' EXIT INT TERM
# Extract field from plutil -p output
pget() { awk -F' => ' "/$1"'[[:space:]]*=>/ {gsub(/^[[:space:]]*"|"$/, "", $2); print $2; exit}' "$2"; }
# Output a single record
output_record() {
local rec_id="$1" identifier="$2" delivered="$3"
[[ -z "$rec_id" ]] && return
local bin="$TMPDIR/$rec_id.bin" json="$TMPDIR/$rec_id.json" ptxt="$TMPDIR/$rec_id.ptxt" use_json
sqlite3 "$DB" "SELECT writefile('$bin', data) FROM record WHERE rec_id = $rec_id;" >/dev/null
if plutil -convert json -o "$json" "$bin" 2>/dev/null && [[ -s "$json" ]]; then
use_json=1
else
use_json=0
plutil -p "$bin" > "$ptxt" 2>/dev/null || true
fi
# Skip records with no usable data
if (( use_json )) && [[ ! -s "$json" ]]; then return; fi
if (( ! use_json )) && [[ ! -s "$ptxt" ]]; then return; fi
if (( JSONL )); then
if (( use_json )); then
jq -c --arg rid "$rec_id" --arg del "$delivered" --arg ident "$identifier" '{
rec_id: ($rid|tonumber), delivered: (if $del != "" then ($del|tonumber) else null end), identifier: (if $ident != "" then $ident else .app end),
app: .app, date: .date, title: .req.titl, subtitle: .req.subt, body: .req.body, dest: .req.dest
}' "$json"
else
local titl subt body app date
titl=$(pget '"titl"' "$ptxt"); subt=$(pget '"subt"' "$ptxt")
body=$(pget '"body"' "$ptxt"); app=$(pget '"app"' "$ptxt"); date=$(pget '"date"' "$ptxt")
jq -nc --arg rid "$rec_id" --arg del "$delivered" --arg ident "$identifier" \
--arg app "$app" --arg date "$date" --arg titl "$titl" --arg subt "$subt" --arg body "$body" '{
rec_id: ($rid|tonumber), delivered: (if $del != "" then ($del|tonumber) else null end), identifier: (if $ident != "" then $ident else $app end),
app: $app, date: (if $date != "" then ($date|tonumber? // $date) else null end),
title: (if $titl != "" then $titl else null end), subtitle: (if $subt != "" then $subt else null end),
body: (if $body != "" then $body else null end)
}'
fi
elif (( ONELINE )); then
if (( use_json )); then
jq -r --arg rid "$rec_id" --arg del "$delivered" --arg ident "$identifier" '
def n: if . == null then "null" else tostring end;
"rec_id=\($rid) delivered=\($del) app=\(if $ident != "" then $ident else (.app|n) end) titl=\(.req.titl|n) subt=\(.req.subt|n) body=\(.req.body|n)"
' "$json"
else
local titl subt body app
titl=$(pget '"titl"' "$ptxt"); subt=$(pget '"subt"' "$ptxt")
body=$(pget '"body"' "$ptxt"); app=$(pget '"app"' "$ptxt")
printf 'rec_id=%s delivered=%s app=%s titl=%s subt=%s body=%s\n' \
"$rec_id" "$delivered" "${identifier:-${app:-null}}" "${titl:-null}" "${subt:-null}" "${body:-null}"
fi
else
echo "==== rec_id=$rec_id app=$identifier delivered=$delivered ===="
if (( VERBOSE )); then
plutil -p "$bin"
elif (( use_json )); then
jq -r 'def n: if . == null then "null" else tostring end;
"app: \(.app|n)\ndate: \(.date|n)\ntitl: \(.req.titl|n)\nsubt: \(.req.subt|n)\nbody: \(.req.body|n)"' "$json"
else
printf "app: %s\ndate: %s\ntitl: %s\nsubt: %s\nbody: %s\n" \
"$(pget '"app"' "$ptxt")" "$(pget '"date"' "$ptxt")" "$(pget '"titl"' "$ptxt")" "$(pget '"subt"' "$ptxt")" "$(pget '"body"' "$ptxt")"
fi
echo
fi
}
# Build WHERE clause
where_clause() {
local min_id="${1:-0}" clauses=()
(( min_id > 0 )) && clauses+=("r.rec_id > $min_id")
[[ -n "$APP_FILTER" ]] && clauses+=("a.identifier = '${APP_FILTER//\'/\'\'}'")
(( ${#clauses[@]} )) && { IFS=' AND '; echo "WHERE ${clauses[*]}"; } || echo ""
}
# Query and output records
query_records() {
local min_id="${1:-0}" where limit
where=$(where_clause "$min_id")
limit=$([[ "$min_id" -gt 0 ]] && echo "" || echo "LIMIT $COUNT")
sqlite3 "$DB" "SELECT r.rec_id, a.identifier, r.delivered_date FROM record r LEFT JOIN app a ON r.app_id = a.app_id $where ORDER BY r.rec_id ASC $limit;" | \
while IFS='|' read -r rec_id identifier delivered; do
output_record "$rec_id" "$identifier" "$delivered"
done
}
if (( FOLLOW )); then
# Initial dump (ordered by rec_id desc, then we reverse for display)
last_id=$(sqlite3 "$DB" "SELECT COALESCE(MAX(rec_id), 0) FROM record;")
start_id=$((last_id - COUNT))
(( start_id < 0 )) && start_id=0
query_records "$start_id"
# Watch for changes
while true; do
printf '%s\n' "$DB" "$DB-wal" "$DB-shm" 2>/dev/null | entr -pnzd true 2>/dev/null || true
new_max=$(sqlite3 "$DB" "SELECT COALESCE(MAX(rec_id), 0) FROM record;")
if (( new_max > last_id )); then
query_records "$last_id"
last_id=$new_max
fi
done
else
# Normal mode - just dump recent records (newest first)
where=$(where_clause 0)
sqlite3 "$DB" "SELECT r.rec_id, a.identifier, r.delivered_date FROM record r LEFT JOIN app a ON r.app_id = a.app_id $where ORDER BY r.delivered_date DESC LIMIT $COUNT;" | \
while IFS='|' read -r rec_id identifier delivered; do
output_record "$rec_id" "$identifier" "$delivered"
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment