As the last step in bringing back my A Full History of macOS (OS X) release dates and rates post, I wanted a way to be notified of released updates. On the Macs I regularly use, of course, this happens automatically for the versions I'm using. But Apple updates the older OSes on a regular basis, and I don't see those releases.
Of late, Apple has been good about listing all their releases—even those without security-related components—on their Apple security releases page, so I though that'd be a good one to watch.
There are lots of tools and web sites out there that monitor pages for changes, but they all seemed overly complicated to me, or do way more than I need. I did mostly like urlwatch, but its output is just raw diff results. I wanted something a bit simpler to read.
I only want to watch one section of the Apple page—the table in the "Apple security updates" section of the page; any other changes are irrelevant. Given enough time, I probably could have muddled my way to an ugly solution that used page scraping and text comparisons, and which would work well some of the time :).
Instead, I chose to ask Claude Code to write me a script that would watch that section of the page for changes. Here's what it came up with, a few seconds later…
Warning: The following code was 100% AI-generated. Use at your own risk; it has not been audited for errors and may be capable of destroying the planet if used with the wrong parameters. Seriously, though, this is not code I wrote, and I can't vouch for it, but it is just a bunch of text manipulation functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | #!/usr/bin/env bash # # Generated by Claude Code URL="https://support.apple.com/en-us/100100" BASELINE="/tmp/apple-security-table-baseline.txt" NEWER="/tmp/apple-security-table-newer.txt" REPORT="/tmp/apple-security-table-report.txt" PYSCRIPT=$(cat <<'PYEOF' import sys import html from html.parser import HTMLParser class TableExtractor(HTMLParser): def __init__(self): super().__init__() self.in_target_div = False self.in_table = False self.rows = [] self.current_row = [] self.current_cell = [] self.in_cell = False def handle_starttag(self, tag, attrs): attrs_dict = dict(attrs) if tag == 'div': classes = attrs_dict.get('class', '') if 'table-wrapper' in classes and 'gb-table' in classes: self.in_target_div = True if self.in_target_div: if tag == 'table': self.in_table = True if self.in_table and tag in ('td', 'th'): self.in_cell = True self.current_cell = [] def handle_endtag(self, tag): if self.in_table: if tag in ('td', 'th') and self.in_cell: cell_text = ' '.join(''.join(self.current_cell).split()) self.current_row.append(cell_text) self.in_cell = False self.current_cell = [] elif tag == 'tr' and self.current_row: self.rows.append(self.current_row) self.current_row = [] elif tag == 'table': self.in_table = False self.in_target_div = False def handle_data(self, data): if self.in_cell: self.current_cell.append(data) with open(sys.argv[1]) as f: content = f.read() parser = TableExtractor() parser.feed(content) if not parser.rows: print("ERROR: No table found", file=sys.stderr) sys.exit(1) for row in parser.rows: print('\t'.join(row)) PYEOF ) # Fetch the page and extract the table using Python extract_table() { local tmphtml tmphtml=$(mktemp /tmp/apple-security-page.XXXXXX.html) curl -s -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" "$URL" -o "$tmphtml" python3 -c "$PYSCRIPT" "$tmphtml" local rc=$? rm -f "$tmphtml" return $rc } echo "Fetching Apple security updates page..." TABLE_CONTENT=$(extract_table) if [ $? -ne 0 ] || [ -z "$TABLE_CONTENT" ]; then echo "ERROR: Failed to fetch or parse the table. Check your internet connection." >&2 exit 1 fi if [ ! -f "$BASELINE" ]; then echo "$TABLE_CONTENT" > "$BASELINE" ROW_COUNT=$(echo "$TABLE_CONTENT" | wc -l | tr -d ' ') echo "First run: baseline saved to $BASELINE ($ROW_COUNT rows)." exit 0 fi echo "$TABLE_CONTENT" > "$NEWER" # Generate report { echo "Apple Security Updates Table Diff Report" echo "Generated: $(date)" echo "URL: $URL" echo "========================================" echo "" if diff -q "$BASELINE" "$NEWER" > /dev/null 2>&1; then echo "No changes detected." else BASELINE_ROWS=$(wc -l < "$BASELINE" | tr -d ' ') NEWER_ROWS=$(wc -l < "$NEWER" | tr -d ' ') echo "Changes detected!" echo " Baseline rows: $BASELINE_ROWS" echo " Current rows: $NEWER_ROWS" echo "" # Lines only in newer (added rows) ADDED=$(comm -13 <(sort "$BASELINE") <(sort "$NEWER")) REMOVED=$(comm -23 <(sort "$BASELINE") <(sort "$NEWER")) if [ -n "$ADDED" ]; then echo "--- ADDED rows (present in current, not in baseline) ---" echo "$ADDED" | while IFS=$'\t' read -r -a cols; do echo " + $(IFS=$'\t'; echo "${cols[*]}")" done echo "" fi if [ -n "$REMOVED" ]; then echo "--- REMOVED rows (present in baseline, not in current) ---" echo "$REMOVED" | while IFS=$'\t' read -r -a cols; do echo " - $(IFS=$'\t'; echo "${cols[*]}")" done echo "" fi echo "--- Raw diff (baseline vs current) ---" diff "$BASELINE" "$NEWER" fi } > "$REPORT" cat "$REPORT" echo "" echo "Report saved to $REPORT" echo "Current table saved to $NEWER" echo "To reset baseline: cp $NEWER $BASELINE" |
Note: I was aware of an HTML parser for Python called Beautiful Soup, which makes HTML parsing a simple affair.
I asked Claude to modify the above to use Beautiful Soup; you can download that version of the script if you prefer. It's about 40 lines shorter, but (obviously) requires you install Beautiful Soup (pip3 install beautifulsoup4) to use it.
I chose to publish the long version, as it has no external dependencies.
The created files are all saved to /tmp, as I have no need to keep this data around, and it's not a big deal if it gets accidentally deleted one day, as it'll get recreated on the next run.
You can use this script as-is by saving it as a shell script then making it executable (chmod a+x /path/to/watcher.sh). To make it even simpler for me to use, though, I embedded it in a Keyboard Maestro macro, and set it to run twice a day.
The macro runs the script, then checks to see if the returned value includes "No changes detected." If so, it puts up a notification to that effect and quits. If there are changes, it displays the script's details on those changes in a new window. It's very basic, but if you want it, here it is.
I can't say how well this actually works yet, as there haven't been any updates released since I installed it. But in testing, it worked great!