Pre-alpha · Open source · Apache-2.0

A second reviewer for every dependency update.

Garnet Guard checks every dependency update against the last release you trusted, and tells you when it looks different in ways that matter.

Trust the last release. Check the next.

Nothing is published to PyPI yet. The comment on the right is what the first release posts for the real lightning 2.6.2 compromise, taken from the replay below.

github-actions bot commented

Garnet Guard: 1 update needs a look

⚠ Warning

lightning 2.6.1 → 2.6.2

  • GRN-C003 2.6.2 adds import-time calls to sensitive APIs: lightning/__init__.py:14 calls subprocess.Popen (in _run_runtime(), reached at import).
  • GRN-T004 github.com/Lightning-AI/pytorch-lightning has a tag for 2.6.1 but none for 2.6.2.

Garnet Guard · what is this? · GRN-C003 · GRN-T004

Rendered the way GitHub shows it. A finding is amber because it is a reason to look, not a verdict.

Replay results

Replayed 2026 compromises flagged
3 of 3from content and tags alone
Routine updates that warned
17 of 9741.7% across the top 100 PyPI packages; 5 were wrong
Package code executed
NoneGarnet reads releases as bytes

Replayed on 2026-10-10 with uv run python -m backtest. The routine set is the last 11 releases of each of the top 100 PyPI packages. Of the 17 warnings, 13 pointed at a real change that turned out benign and 5 were wrong; every one is read and written up in the triage file. The 20 clean updates around the incidents all passed. Full incident replay output.

Replays

Each replay downloads the release a project trusted and the compromised release that followed, checks both against the hashes PyPI published, and diffs them. Nothing is run. This is the actual output.

lightning 2.6.1 → 2.6.2 warn

PyPI · April 30, 2026 · MAL-2026-3201

Uploaded with a stolen token. A thread started in lightning/__init__.py launches a Bun-based credential stealer the moment anything imports the package. Install-script blocking does not help here, because the payload runs at import, not at install.

garnet diff lightning 2.6.1 2.6.2
lightning 2.6.1 → 2.6.2: WARN
  WARN  GRN-C003 new-import-time-exec: 2.6.2 adds import-time calls to sensitive APIs in 1 module(s)
           - lightning/__init__.py:14 calls subprocess.Popen (in _run_runtime(), reached at import)
  WARN  GRN-T004 no-matching-tag: github.com/Lightning-AI/pytorch-lightning has a tag for 2.6.1 but none for 2.6.2
           - 2.6.1 → tag 2.6.1 at 655a3a91828694b9bfc241177f474741e483aeaa
           - 2.6.2: no tag named 2.6.2, v2.6.2, lightning-2.6.2 or lightning-v2.6.2

verdict: warn

What this replay could not see

GRN-T001 (lost provenance) cannot fire here: lightning did not publish attestations before the incident, so there was no provenance to lose. GRN-T004 is evaluated against the repository's tags as they exist today, not as they stood on April 30.

lightning 2.6.1 → 2.6.3 warn

PyPI · April 30, 2026 · MAL-2026-3201

A second upload of the same payload minutes later, with no git tag and no workflow run. Garnet compares it with 2.6.1, the last release the project would have trusted.

garnet diff lightning 2.6.1 2.6.3
lightning 2.6.1 → 2.6.3: WARN
  WARN  GRN-C003 new-import-time-exec: 2.6.3 adds import-time calls to sensitive APIs in 1 module(s)
           - lightning/__init__.py:14 calls subprocess.Popen (in _run_runtime(), reached at import)
  WARN  GRN-T004 no-matching-tag: github.com/Lightning-AI/pytorch-lightning has a tag for 2.6.1 but none for 2.6.3
           - 2.6.1 → tag 2.6.1 at 655a3a91828694b9bfc241177f474741e483aeaa
           - 2.6.3: no tag named 2.6.3, v2.6.3, lightning-2.6.3 or lightning-v2.6.3

verdict: warn

What this replay could not see

The same limits as 2.6.2: no earlier attestations for GRN-T001 to compare against, and tags as they exist today.

elementary-data 0.23.2 → 0.23.3 warn

PyPI · April 24, 2026 · MAL-2026-3083

Published by the project's own CI after a GitHub Actions script injection, so the upload path looked normal. The release adds elementary.pth, which runs a base64 payload every time Python starts, for every program on the machine.

garnet diff elementary-data 0.23.2 0.23.3
elementary-data 0.23.2 → 0.23.3: WARN
  WARN  GRN-C001 new-pth-file: 0.23.3 adds code that runs at every Python startup (.pth)
           - new elementary.pth (246,691 bytes, 1 executable line(s)): import os, subprocess, tempfile, base64; x = os.path.join(tempfile.gettempdir(), ".trinny-security-update"); …
  WARN  GRN-T004 no-matching-tag: github.com/elementary-data/elementary has a tag for 0.23.2 but none for 0.23.3
           - 0.23.2 → tag v0.23.2 at c642a369fbe1d1be2ae9eb5e981b8863ece21bed
           - 0.23.3: no tag named 0.23.3, v0.23.3, elementary-data-0.23.3 or elementary-data-v0.23.3

verdict: warn

What this replay could not see

Because the project's own pipeline published the release, provenance checks alone would have passed it. The signal is in the content: the new .pth file. GRN-T004 is evaluated against tags as they exist today.

20 clean updates pass

Routine releases of popular packages, including the clean releases on either side of each incident.

A reviewer that warns on everything gets ignored. These updates produced no warning from a detection rule.

  • lightning 2.6.4 → 2.6.5
  • lightning 2.6.5 → 2.6.6
  • elementary-data 0.23.2 → 0.23.4
  • requests 2.32.4 → 2.32.5
  • urllib3 2.4.0 → 2.5.0
  • certifi 2025.7.14 → 2025.8.3
  • idna 3.9 → 3.10
  • attrs 25.3.0 → 25.4.0
  • packaging 25.0 → 26.0
  • jinja2 3.1.5 → 3.1.6
  • click 8.2.1 → 8.3.0
  • pydantic 2.11.7 → 2.12.0
  • httpx 0.28.0 → 0.28.1
  • setuptools 80.8.0 → 80.9.0
  • django 5.2.6 → 5.2.7
  • numpy 2.3.3 → 2.3.4
  • boto3 1.40.40 → 1.40.41
  • rich 14.1.0 → 14.2.0
  • sigstore 4.0.0 → 4.1.0
  • pypi-attestations 0.0.28 → 0.0.29

Malicious files are not hosted here. The replay fetches them from files.pythonhosted.org by URL and SHA-256, listed in backtest/incidents.toml, using hashes from Datadog's malicious-software-packages-dataset. Advisories are from OSV.

How it works

The attacks that hurt in 2025 and 2026 were not new packages. They were new versions of packages people already trusted, and each one looked different from the last good release in ways a machine can check.

Trust downgrade

The new release has no provenance attestation when the old one did, was built by a different repository or workflow, or has no matching git tag.

GRN-T001GRN-T002GRN-T003GRN-T004

Risky content change

A new .pth file, a new build hook, or new code that runs a process, loads code or uses the network at import time.

GRN-C001GRN-C002GRN-C003

Known bad

The release matches an OSV malicious-package advisory, so a feed has already confirmed it.

GRN-K001

  • Never runs the code it inspects. Releases are read as bytes and parsed with ast.
  • “Couldn't check” is never “pass”. A fetch or parse that fails is reported as a finding.
  • Quiet on clean updates. Passing packages collapse to one line in the comment.
  • Explainable. Every finding names a rule and its evidence, and garnet explain says what to do.

What the replayed rules say

From garnet explain. Every rule has a page at /rules/GRN-XXXX.

GRN-C001 new-pth-file
Python runs import lines in .pth files in site-packages every time the interpreter starts, for every program, before any of your code. Almost no package needs a new one. What to do: read the .pth file. If it is a legitimate path hook (editable installs, namespace helpers) the release notes usually say so; otherwise treat the release as compromised.
GRN-C003 new-import-time-exec
Module top-level code runs the moment anything imports the package, including your test suite and CI. What to do: read the flagged lines. Network or subprocess calls at import are rare in libraries; ask whether the release notes explain them.
GRN-T001 lost-provenance
The trusted version was published by CI with a Sigstore attestation proving which repository and workflow built it. A new file with no attestation was most likely uploaded some other way, for example with a stolen or leaked API token. What to do: don't merge yet. Check the project's repository for a release workflow run and a tag for this version.
GRN-T004 no-matching-tag
Projects that tag releases normally tag every release. A version on the registry with no tag was probably not built from the repository's history, which is typical of a token-based upload. What to do: check the repository's tags and releases page. A missing tag can be an oversight, so confirm with the maintainers before merging.

Get started

Garnet Guard is pre-alpha. You can run the prototype from source today; the PyPI package and the GitHub Action come with the first release.

1 Today, from source

$ git clone https://github.com/orca-solutions/garnet-guard
$ cd garnet-guard && uv sync
$ uv run garnet diff lightning 2.6.1 2.6.2
$ uv run python -m backtest   # replay every incident

This block becomes the terminal recording when it is ready.

2 First release: one command planned

uvx garnet-guard check --git-base origin/main

Compares your lockfile with the one on main. Works with uv.lock, poetry.lock, pylock.toml and pinned requirements.txt.

3 On every Dependabot PR planned

permissions:
  contents: read
  pull-requests: write
steps:
  - uses: orca-solutions/garnet-guard@<sha>
    with:
      fail-on: block

npm support follows Python. Agent hooks for Claude Code and Codex, which stop an agent from quietly adding a risky dependency, are on the roadmap.

Open, and staying that way

  • Apache-2.0. The whole engine and every rule.
  • No telemetry. No account, no API key, nothing phones home. This page loads nothing from third parties either.
  • Detection is never paywalled. Free for public and private repositories. See OPEN_CORE.md.
  • Complementary. Keep your cooldown, pip-audit and malware feeds. Garnet answers a different question: is this release built and shaped like the last one you trusted?