Your code scanning alerts got wiped by a re-scan, a migration, or a well-meaning intern. This tool brings them back. One command. No drama.
gh extension install ghcli/gh-replay-alerts
pip install tqdm defusedcsv requests# Backup alert states (do this BEFORE you touch anything)
gh replay-alerts list owner/repo --scope repo > backup.csv
# Restore from backup
cat backup.csv | gh replay-alerts owner/repo --scope repo
# Reopen all dismissed alerts
gh replay-alerts list owner/repo --scope repo \
| sed 's/,dismissed,/,open,/' \
| gh replay-alerts owner/repo --scope repo
# Migrate triage from GHES to GHEC
gh replay-alerts list owner/repo --scope repo --hostname ghes.corp.com > export.csv
cat export.csv | gh replay-alerts owner/repo --scope repo
# Dry run (see what would happen without changing anything)
cat backup.csv | gh replay-alerts owner/repo --scope repo --debug 2>&1 | grep -E "^(INFO|WARNING)"
# Org-wide backup
gh replay-alerts list my-org --scope org > org-backup.csv
# Show help
gh replay-alerts helpReads a CSV of previous alert states. Fetches current alerts from GitHub. Matches them. PATCHes the mismatches. No magic. Just the API, used correctly.
Matching uses a cascading strategy because the world is messy:
- Alert number (primary) — stable across code changes. Extracted from the URL.
- File location (fallback) —
(path, line, column). For when alert numbers change after a re-scan or tool upgrade.
The summary tells you which strategy matched each alert. You are never guessing.
| Command | What it does |
|---|---|
gh replay-alerts list <name> [opts] |
Export alerts as CSV or JSON |
gh replay-alerts <name> [opts] |
Replay states from CSV on stdin |
gh replay-alerts help |
Show usage |
| Option | Applies to | Description |
|---|---|---|
--scope repo|org|ent |
both | Scope of the query (default: org) |
--state open|resolved |
both | Filter by state |
--since DATE|Nd |
both | Only alerts after date (2024-10-08, 7d) |
--hostname HOST |
both | GHES hostname (default: github.com) |
--debug |
both | Verbose logging |
--json |
list | JSON instead of CSV |
--raw |
list | Raw API JSON |
--quote-all |
list | Quote all CSV fields |
Every run prints a diagnostic summary. No silent failures. No guessing.
INFO: CSV loaded: 2650 rows, 1 repos, 2650 unique files, 2650 with alert numbers
INFO: === Replay Summary ===
INFO: API alerts processed: 12
INFO: Matched to CSV: 8
INFO: By alert number: 7
INFO: By location: 1
INFO: State already same: 6
INFO: State changed: 2
INFO: Unmatched: 4
INFO: Repo not in CSV: 0
INFO: Path not in CSV: 1
INFO: Location mismatch: 3
INFO: ======================
Zero matches? It tells you why:
WARNING: Zero matches found. Common causes:
WARNING: 1. Alerts were re-generated by a new scan (different alert numbers)
WARNING: 2. CSV repo name doesn't match API repo name
WARNING: 3. CSV was generated from a different branch or scan
WARNING: → 12 alerts matched repo+path but NOT line/column — code edits shifted locations
Exits with code 1 when CSV has data but nothing matched. Because silent success on total failure is a bug, not a feature.
The classic. Someone clicked "Re-run all jobs." Your carefully triaged false positives? Back to open. Your weekend plans? Also open to interpretation.
cat backup_before_rescan.csv | gh replay-alerts owner/repo --scope repoSet up a cron. Future-you will thank present-you:
gh replay-alerts list owner/repo --scope repo > "alerts-$(date +%Y-%m-%d).csv"Migration tools move code. They do not move dismissal states. Your security team spent 3 sprints triaging 4,000 alerts. The migration tool said success. The dashboard said 4,000 open. The security team said things we cannot print.
gh replay-alerts list owner/repo --scope repo --hostname ghes.company.com > triage.csv
cat triage.csv | gh replay-alerts owner/repo --scope repoNo judgment. We have all been the intern.
gh replay-alerts list owner/repo --scope repo \
| sed 's/,dismissed,/,open,/' \
| gh replay-alerts owner/repo --scope repoMajor version upgrades regenerate alerts with fresh IDs. Your backup CSV has the right triage decisions. The cascading strategy matches by location when numbers change.
cat pre_upgrade_backup.csv | gh replay-alerts owner/repo --scope repo --debugTriage one golden repo. Replay everywhere.
gh replay-alerts list owner/golden-repo --scope repo > golden.csv
for repo in repo-1 repo-2 repo-3 repo-4 repo-5 repo-6 repo-7 repo-8 repo-9; do
cat golden.csv | gh replay-alerts "owner/$repo" --scope repo
doneCompliance wants a CSV with timestamps, who dismissed what, and why. They want it yesterday.
gh replay-alerts list owner/repo --scope repo > "audit-$(date +%Y%m%d).csv"master to main can reset alert states because alerts are branch-scoped.
gh replay-alerts list owner/repo --scope repo > pre-rename.csv
# rename branch
cat pre-rename.csv | gh replay-alerts owner/repo --scope repoSame code, fresh alert numbers. Location fallback handles it.
cat old_repo_backup.csv | gh replay-alerts owner/new-repo --scope repo --debugClear the deck temporarily. Restore after.
gh replay-alerts list owner/repo --scope repo > pre-release.csv
# dismiss the noise via API
cat pre-release.csv | gh replay-alerts owner/repo --scope repoYou do not know what changed. You do not have time to find out. But you have last night's backup.
cat alerts-2026-03-22.csv | gh replay-alerts owner/repo --scope repogh extension install ghcli/gh-replay-alerts
pip install tqdm defusedcsv requestsRequires GitHub CLI (gh auth login) and Python 3.10+.
MIT