Colorado hail season means storms can go from "clear sky" to "golf-ball hail on my garden and my car" in under an hour, and by the time the sirens or the news catch up it's already happened. I built hailAlert to get ahead of that — for my house in Fort Collins, and for the ~10 Colorado/Wyoming ski areas I track (Eldora, Winter Park, Loveland, A-Basin, Keystone, Breckenridge, Copper, Vail, Beaver Creek, Steamboat, Snowy Range) so I know snow risk there too, not just hail at home.
What it watches
- Pulls NWS active alerts for each location's exact lat/lon.
- Pulls SPC's Day-1 hail outlook (the "how likely is hail today, this spot" probability layer).
- For the home location specifically, it also tracks hard freeze, frost (shoulder-season plant protection), and gives a daily 7am brief.
- For the ski locations, it tracks active alerts plus a rolling snowfall forecast.
Why I built it
I don't want to be the guy checking weather.gov every hour during storm season, and I don't want a generic weather app that treats a marginal risk day the same as a warning polygon sitting directly on top of my house. I wanted three tiers, tuned to what actually matters to me:
| Tier | Trigger |
|---|---|
| Heads-up | SPC Day-1 hail probability over my point ≥ 15% |
| Watch | NWS Severe Thunderstorm/Tornado Watch, or a severe-grade special weather statement mentioning hail |
| URGENT | NWS Severe Thunderstorm or Tornado Warning whose polygon actually contains my point |
It runs every 3 minutes off cron, dedupes by NWS alert ID so I only get pinged once per event, and pushes to my phone via ntfy.sh and to my desktop. Stdlib-only Python, single script, config-driven — I can add or move a location by editing config.json, no redeploy needed.
What it deliberately doesn't do (yet)
I left out MRMS/MESH radar-derived hail (ships as GRIB2, fragile to parse, and NWS warnings already fold radar + human spotters in) and storm-motion ETA math (the NWS warning polygon already encodes where the storm's headed for the next 30–60 min). Server-side deployment so it keeps running when my laptop's asleep is planned as phase 2.
What an alert actually looks like
Here's the real message format the code builds (from rule_active_alerts in hail_alert.py) — this is the template shape, reconstructed from the actual code, not a transcript of a real event:
Title: <location name> — <NWS event name> (hail to <N>") Body: Until: <expiration time> Issued by: <NWS office>
Example of the shape (not a real fired alert): Home (Coventry, FC) — Severe Thunderstorm Warning (hail to 1.5")
Every log line in logs/hail_alert.log currently shows fired=0. state/hail_alert_state.json has an empty dedup set — nothing has ever actually fired and deduped for real. The only "alert" evidence anywhere in the repo is a synthetic one from a manual --test run: TEST - Severe Thunderstorm Warning - hail to 1.5" [urgent], logged 2026-05-29. That's a smoke test, not a real hail event, and it isn't presented above as one. web/state.json currently shows the live banner as GREEN / "No active threats."
- This post ships as a build log, not a "here's it catching a real storm" story.
- When a real severe-weather day trips it, I'll add an actual screenshot of the fired ntfy push, clearly dated, in place of the template above.