Daily scanner for critical GitHub issues across public repos you depend on. Catches hardware-killing bugs, security vulnerabilities, and regressions before they hit the tech press two months later.
Born from the Codex SSD incident where TRACE-level logging silently wrote ~640 TB/year to developers' drives. The first GitHub issue was filed in April 2026; most developers didn't hear about it until late June.
- Reads
watchlist.jsonin this repo. - Fetches recently active issues from those public repos via the GitHub API.
- Scores each issue by severity signals: destructive keywords, label matching, community reaction velocity, and recency.
- Merges results into a persisted scan state so the next run knows what changed.
- Outputs a ranked HTML report in the scheduled GitHub Actions run.
- Exits with code 1 when the scan finds a meaningful attention transition, so the scheduled run becomes a visible alert without repeating the full active set.
The same scanner can also run behind the included local report server. In that mode, the browser requests a report from the server; GitHub credentials and scan state never go to the browser.
You do not install anything into the repos being watched. A watched repo is just
an owner/repo string in watchlist.json.
Preview generated from local scan state with two representative attention transitions. The complete active set remains available under All active.
- Fork or clone this repo.
- Edit
watchlist.jsonand add the public repos you care about. - Enable GitHub Actions for the repo.
- Run the
issue-watcherworkflow manually, or wait for the daily schedule. - Open the
issue-watcher-reportartifact from the workflow run.
To add a public repo, edit watchlist.json:
{
"repos": [
"openai/codex",
"microsoft/vscode",
{
"owner": "vercel",
"repo": "next.js",
"extraKeywords": ["turbopack", "build failure"]
}
],
"lookbackDays": 1,
"minSeverity": 3,
"format": "terminal"
}The object form lets you add repo-specific severity keywords without changing the scanner code.
The CLI is still useful for testing scanner changes locally:
npm install
npm test
npm run lint
npm run typecheck
npm run scan
npm run html
npm run build
node dist/main.js watchlist.jsonYou can also create an ignored config.json for private local overrides:
cp config.example.json config.json
npm run devThe local scan state is written to state/issues-state.json by default. Pass
--state path/to/file.json to use a different state file.
To run a real scan and open the report in a browser:
npm run servePublic repositories do not require a token. Set GITHUB_TOKEN when you want the
higher authenticated rate limit.
Open http://127.0.0.1:8765/. The first page load performs a scan. The result is held in memory for subsequent page loads, while the existing state file continues to provide ETag and updated-at caching. Use the report's Scan now control to force a fresh scan.
The repository rail distinguishes a baseline, a fresh update, an unchanged ETag response, an empty scan window, and an unavailable repository. The report opens on the focused Attention view; All active exposes the complete stored signal set. Severity filters select exact categories rather than minimum thresholds.
The server exposes these local endpoints:
GET /returns the live HTML report.GET /api/reportreturns the current cached report as JSON.POST /api/scanforces a scan and returns the new report as JSON.GET /healthreturns a simple server health response.
The server binds to 127.0.0.1:8765 by default. Pass --host, --port, and
--state after npm run serve -- to change those settings. Do not expose the
server publicly without adding authentication and HTTPS.
The test suite uses Node's built-in test runner:
npm testThe focused tests cover:
- config parsing and validation
- stable GitHub issue request parameters
If-None-Match/304 Not Modifiedhandling- pull request filtering
- updated-at watermark filtering
Linkheader pagination- persisted scan state load/save behavior
- live report server routing and scan caching
- bounded scoring evidence and overlapping keyword suppression
- baseline suppression and meaningful attention-transition classification
Pull requests also run the ci workflow, which checks:
npm testnpm run typechecknpm run lintnpm run build
The GitHub Actions workflow reads watchlist.json. The local CLI accepts a
config path as the first argument and falls back to watchlist.json when no path
is provided.
{
"repos": [
"openai/codex",
"vercel/next.js",
{
"owner": "prisma",
"repo": "prisma",
"extraKeywords": ["migration", "schema drift"]
}
],
"lookbackDays": 1,
"minSeverity": 3,
"format": "terminal"
}lookbackDays is used as the initial activity window when a repo has no prior
scan state. After the first successful scan, the watcher uses the repo's last
successful scan start time as its watermark. This keeps older but newly active
issues visible without repeatedly walking the whole issue history.
Supported formats are terminal, markdown, json, and html. GitHub Actions
overrides the configured format and writes report.html for the downloadable
artifact.
Each repo scan uses GitHub's repository issues endpoint:
GET /repos/{owner}/{repo}/issues
?state=all
&sort=updated
&direction=desc
&per_page=100
The request is intentionally sorted by updated descending. Pagination stops as
soon as the oldest issue on a page is older than the previous successful scan
watermark.
The watcher stores the page-one ETag and sends If-None-Match on later runs.
When GitHub returns 304 Not Modified, the watcher reuses its previous issue
state for that repo and skips pagination.
GitHub documents that authenticated 304 Not Modified responses do not count
against the primary rate limit, so the included workflow sends requests with the
built-in GITHUB_TOKEN. Local unauthenticated runs can still use ETags, but
should not rely on 304 responses preserving the 60-request/hour budget.
GitHub's issues API also returns pull requests, so entries with a pull_request
key are filtered out before scoring.
The scheduled GitHub Actions workflow uses its built-in GITHUB_TOKEN. For
local runs, unauthenticated GitHub API calls allow 60 requests/hour. With a
token, you get 5,000/hour:
export GITHUB_TOKEN=ghp_your_token_hereThe token only needs public repo read access.
Issues are scored by a weighted combination of signals:
| Signal | Weight | Notes |
|---|---|---|
| Critical keyword | 3.0 | "data loss", "corruption", "SSD", etc. |
| Warning keyword | 1.5 | "regression", "crash", "memory leak" |
| Critical label | 3.0 | "security", "critical", "p0" |
| Warning label | 1.0 | "bug", "regression", "performance" |
| Reactions (+1) | 0.1/ea | Capped at 5 points |
| Comments | 0.15/ea | Capped at 4 points |
| Recency boost | 1.0 | Issues created in last 48 hours |
Keyword and label evidence uses case-insensitive term boundaries. Overlapping
matches prefer the longer phrase, so memory leak does not also count the
generic leak term. Each configured term contributes at most once per issue.
Repository-specific extraKeywords are currently treated as critical keyword
evidence.
The default threshold (minSeverity: 3) catches a single critical keyword or
label match. Raise it if you're watching noisy repos.
The active signal set and the attention queue are deliberately different:
- The first successful scan of each repository establishes a baseline. Existing issues are indexed but are not reported as newly discovered alerts.
- A later scan requests attention for a genuinely new issue, a threshold crossing, a move into a higher severity band, newly added keyword or label evidence, or fresh activity on an already-critical issue.
- Reaction and comment changes inside the same non-critical severity band do not create attention items.
- The CLI exits with code 1 only when the attention queue is non-empty. The HTML report keeps All active available as a secondary, collapsed view.
Attention is scoped to meaningful transitions found in the current scan. It is not an acknowledgement or ticket-assignment system. Activity still contributes to the severity score, so an activity increase can trigger attention when it crosses a threshold or severity band.
The included GitHub Actions workflow is the simplest setup, but the CLI can also run from cron, launchd, or any other scheduler.
The workflow restores and saves the state/ directory using GitHub Actions
cache. Attention items are emitted as an Actions notice and do not fail the
scheduled job. Config, network, and scan failures still fail the job, while the
HTML report and updated state are preserved whenever they were produced.
npm run htmlThis writes report.html, a self-contained page with repo, severity, and search
filters for the generated scan results.
For a browser report backed by a real scan, use npm run serve instead.
crontab -e0 8 * * * cd /path/to/github-issue-watcher && /usr/local/bin/node dist/main.js watchlist.json 2>&1 | mail -s "github-issue-watcher daily" you@example.comPipe markdown output to a Slack incoming webhook:
OUTPUT=$(node dist/main.js watchlist.json 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -eq 1 ]; then
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\": \"$(echo "$OUTPUT" | head -c 3000)\"}" \
https://hooks.slack.com/services/YOUR/WEBHOOK/URL
fi| Code | Meaning |
|---|---|
| 0 | Scan succeeded with no attention transitions |
| 1 | New or materially changed issues need attention |
| 2 | Fatal error, config or network |
MIT
