← back to gallery

Tag Drift

Catch force-pushed GitHub Action tags and pin every workflow to immutable SHAs

dev-toolsgithub-actionssupply-chainsecuritysha-pinningtag-mutationdevsecops
Open product ↗

tag-drift

Live dashboard for GitHub Action tag SHA mutations. Watches the SHA pinned by every release tag of the most-used marketplace actions. When a tag's SHA changes — that is, a maintainer (or attacker) force-pushed history — we log it. Includes a paste-and-rewrite tool that converts your floating actions/checkout@v4 into an immutable actions/checkout@<sha> # v4 pin in one click.

Public, zero-auth, runs at <https://holyai.me/tag-drift/>.

---

Why this exists (May 2026)

Tag mutability is the single most underrated GitHub Actions attack class of the year:

GitHub's August 2025 changelog added org-level SHA-pin enforcement, and the 2026 Actions Security Roadmap makes immutable references first-class. But teams still need (a) a way to detect tag mutations across the public marketplace, and (b) a per-action SHA lookup tool. That's tag-drift.

What it does

Real data sources (no mocks)

| Source | URL pattern | Refresh | Notes |
|---|---|---|---|
| GitHub /repos/.../tags | https://api.github.com/repos/{owner}/{repo}/tags?per_page=100 | tier-1 30 min, tier-2 2 h, tier-3 daily | Primary mutation detection signal |
| GitHub /repos/.../releases/latest | https://api.github.com/repos/{owner}/{repo}/releases/latest | same as tags | Identifies canonical "latest stable" tag |
| GitHub /repos/.../git/ref/tags/{tag} | https://api.github.com/repos/{owner}/{repo}/git/ref/tags/{tag} | on-demand (paste-and-rewrite fallback) | Resolves a single tag → SHA |
| GitHub /repos/.../security-advisories | https://api.github.com/repos/{owner}/{repo}/security-advisories | every 4 h | Per-action GHSAs |
| GitHub /advisories | https://api.github.com/advisories?per_page=100&sort=published | hourly | Global GHSA stream |
| GitHub /search/repositories | https://api.github.com/search/repositories?q=topic:github-actions+stars:>=200 | daily | Auto-discover newly popular actions |
| GitHub /repos/{o}/{r} | https://api.github.com/repos/{owner}/{repo} | weekly | Stars, description, archived flag |

If GITHUB_TOKEN is set in the environment, all calls go authenticated (5000 req/h budget). Without a token the unauth limit (60 req/h) is enough for the top-50 tier but throttles the rest.

HTTP API (all under /tag-drift, all unauth)

| Verb | Path | Returns |
|---|---|---|
| GET | /tag-drift/health | { ok, version, uptime_s, actions_tracked, mutations_30d } — health probe |
| GET | /tag-drift/api/stats | Dashboard counters + per-day mutation series + top-mutated list |
| GET | /tag-drift/api/actions | Tracked actions; supports ?q=, ?tier=1|2|3, ?limit=, ?offset= |
| GET | /tag-drift/api/actions/:owner/:repo | Detail: metadata + last 100 observations + last 50 mutations + advisories |
| GET | /tag-drift/api/mutations | Mutation feed; supports ?severity=critical|warn|info, ?since=ISO, ?limit=, ?offset= |
| GET | /tag-drift/api/advisories | Recent action-affecting GHSAs |
| GET | /tag-drift/api/fetch-log | Last 100 fetch attempts (transparency) |
| POST | /tag-drift/api/pin | Body { "yaml": "<workflow>" }{ ok, yaml, summary, changes, unresolved, already_pinned, skipped } |
| GET | /tag-drift/api/recommend/:owner/:repo | { tag, sha, comment_form } for the latest stable tag |
| GET | /tag-drift/api/export.json | Full snapshot, 5-min cached |
| GET | /tag-drift/api/export.csv | Same, CSV |

How to interpret severities

Run locally

npm install
# optional, but recommended:
echo "GITHUB_TOKEN=$YOUR_PAT" >> .env
node server.js
# open http://127.0.0.1:4837/tag-drift/

On first boot the actions table is empty and gets seeded from config/seed-actions.json (≈80 known popular actions). The first cron tick (~30 seconds after boot) starts populating tag observations.

Stack

Tier policy

Differentiation vs. sibling fleet products

License

MIT. Built by Cowork (Claude Opus 4.7) on 2026-05-21 as part of Arda Kutsal's daily R&D pipeline.