Pinwatch
A CLI and GitHub Action that keeps checking your SHA-pinned GitHub Actions after the scan is over, and catches it when the tag behind a pin gets quietly repointed or a fork-triggered job can poison a…
Pinwatch
A CLI and GitHub Action that keeps checking your SHA-pinned GitHub Actions after the scan is over, and catches it when the tag behind a pin gets quietly repointed or a fork-triggered job can poison a cache a privileged job later trusts.
Problem
Pinning a third-party action to a commit SHA is the standard fix for tag-repointing attacks like tj-actions/changed-files (CVE-2025-30066), where 23,000+ repos running @v45 got hit because a tag is just a mutable pointer. Tools like zizmor catch the unpinned case at scan time. But the SHA comment next to the pin, uses: owner/repo@a1b2c3d # v4.2.0, is only true the day someone writes it. Nothing re-checks it six months later, and nothing watches for the attack TanStack hit on May 11, 2026: a pull_request_target workflow let a forked PR poison a shared Actions cache, which a separate, privileged release workflow then restored and trusted, letting the attacker extract an OIDC token straight out of runner memory and publish 84 malicious npm packages without ever touching a maintainer's credentials.
Target user
Platform and security engineers at npm/PyPI-publishing orgs or internal monorepos running dozens of GitHub Actions workflows. This is the team that already adopted zizmor or actionlint after the 2026 wave of supply-chain incidents (TanStack in May, keyv in August, the Microsoft-documented ChainDrop worm), and wants to know what those linters stop checking once the scan is done.
MVP scope
- Parse every
.github/workflows/*.ymlfile and extract alluses: owner/repo@<sha> # <tag>pins - Daily scheduled Action that re-resolves each tag against the GitHub API and diffs the commit it now points to against the SHA recorded in the comment, opening an Issue (never a silent auto-PR) for any pin whose comment no longer matches reality
- Build a reachability graph per repo: flag any job triggered by
pull_request_target,workflow_run, or a fork-writable event that shares a cache key (actions/cache,setup-nodecache, etc.) with a job holdingid-token: writeor secrets access. That's the exact TanStack chain. - SARIF output so findings land in GitHub code scanning next to CodeQL and zizmor results instead of a separate dashboard
- Single-binary CLI plus a drop-in composite Action (
pinwatch initscaffolds the scheduled workflow) - A reviewed-pin allowlist so a deliberate, human-approved tag bump doesn't re-trigger the same alert every day
Monetization
Freemium. The CLI and single-repo scheduled scan are free and open source (MIT). The paid tier is a hosted dashboard that ingests results across every repo in a GitHub org, tracks pin-drift trends over time, and pushes Slack/PagerDuty alerts. Priced flat per org per month, aimed at the platform-security team that owns the org, not billed per seat.
Why now
The TanStack compromise on May 11, 2026 showed that SHA pinning alone doesn't stop a cache-poisoning path between a fork-triggered job and a privileged one. The GitHub Actions hardening checklist published this year recommends SHA pinning but, by its own account, doesn't cover monitoring for retroactive tag repoints or cache-key collisions across workflows. That's the gap Pinwatch closes, continuously instead of once at scan time.
Risks & open questions
- Demand-side: teams that already run zizmor or Dependabot may assume they're covered and won't see the gap until it's explained with a real incident, which is a hard sell before a breach
- Cache-key resolution is the hard engineering problem: keys are often built from templated context expressions that are difficult to resolve statically across workflow boundaries
- Daily re-resolution of every pin across every workflow in a large org risks hitting GitHub's secondary API rate limits without a careful ETag/conditional-request layer
- Legitimate tag movement under a moving major version (
@v4bumping fromv4.1tov4.2) will look identical to a malicious repoint unless the allowlist flow is low-friction enough that teams actually use it
Next step
Build the SHA-vs-tag drift checker first against a sample of real public repos' workflow history to measure false-positive rate before investing in the harder cache-reachability graph.
Sources
- https://tanstack.com/blog/npm-supply-chain-compromise-postmortem — mechanism of the May 11, 2026 TanStack compromise: pull_request_target plus cache poisoning plus OIDC token extraction from runner memory
- https://www.stingrai.io/blog/github-actions-security-checklist — 2026 GitHub Actions hardening checklist; explicitly does not cover monitoring for retroactive SHA/tag repointing, the gap this idea targets
- https://makandracards.com/makandra/625715-zizmor-static-analysis-github-actions — zizmor's tag-vs-SHA pinning check and the tj-actions/changed-files CVE-2025-30066 precedent (23,000+ repos affected via a moved tag)