Skip to main content

unripe

PyPI CI License: MIT

unripe checks your lockfiles and fails if any dependency version was published too recently. The default is 3 days. It works across npm, pnpm, yarn, bun, PyPI, Cargo, Go, Composer and RubyGems, so a repo with a Python backend and a JavaScript frontend gets one check and one policy instead of five different settings.

Here it is on Discourse's Gemfile.lock, the evening of October 20, 2026:

$ unripe check
unripe: checked 339 versions in 1 lockfile, minimum age 3d

unripe: 2 versions published less than 3d ago
  rubygems aws-sdk-core 3.259.0  7h old, published 2026-10-20 19:21 UTC, old enough at 2026-10-23 19:21 UTC
           Gemfile.lock:1014
  rubygems logster 3.0.0         35h old, published 2026-10-19 15:32 UTC, old enough at 2026-10-22 15:32 UTC
           Gemfile.lock:1126

fail: 2 unripe, 0 unknown, 337 ok, 0 excluded

Nothing is wrong with either of those gems. That's normal. Most new versions are fine. The point is that when one isn't, you find out after everyone else does, not before.

Why bother

When a popular package gets hijacked, the bad version is usually spotted and pulled within hours. In August 2026 a worm went through npm and published malicious versions of 444 packages in about four hours, stealing CI and cloud credentials from whoever installed them. In March, two backdoored LiteLLM releases sat on PyPI for somewhere between 40 minutes and three hours. Waiting a few days before taking a new version would have skipped both.

Most package managers have added a setting for this in the last year, and if yours has one you should turn it on. unripe is for what those settings don't do. It checks what is already in the lockfile, including whatever a bot or an old tool put there. It reports on just the versions a pull request adds. It applies one policy to every ecosystem in the repo. And it covers Go and Composer, which don't have a setting yet.

Install

You need Python 3.11 or newer. unripe has no dependencies, on purpose: a tool that checks your supply chain shouldn't add to it.

uvx unripe check

or pipx install unripe, or pip install unripe.

Lockfiles it reads

File Publish times come from
package-lock.json, npm-shrinkwrap.json npm registry
pnpm-lock.yaml npm registry
yarn.lock (classic and berry) npm registry
bun.lock npm registry
uv.lock the lockfile's own upload times
poetry.lock, Pipfile.lock PyPI
requirements*.txt, constraints*.txt PyPI (exact == pins only)
Cargo.lock crates.io sparse index
go.mod Go module proxy and checksum database
composer.lock the lockfile's own time field, else Packagist
Gemfile.lock rubygems.org

Dependencies from git, a local path, a workspace or another registry are skipped, since there's nothing public to ask about them. The old binary bun.lockb can't be read; bun 1.2 and later write a text bun.lock.

For Go it reads go.mod, not go.sum. Since Go 1.17, go.mod lists every module in the build, while go.sum also remembers versions that aren't used.

In GitHub Actions

name: unripe
on: pull_request

permissions:
  contents: read

jobs:
  unripe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: TexWoW/unripe@v0.2.0
        with:
          min-age: 3d

On a pull request it only looks at versions the PR adds, compared with the base commit. It fetches that commit if the checkout is shallow, so you don't need fetch-depth: 0. On a push it checks everything. The report goes to the job summary.

Pin the action to a full commit sha rather than a tag if you can. The action runs unripe from its own checkout, so the code that runs is the commit you pinned, and nothing gets downloaded from PyPI.

Inputs:

Input Default
min-age 3d 90m, 72h, 3d, 1w
since PR base a git ref, or none to check everything
paths whole repo files or directories, space or newline separated
exclude patterns, one per line
unknown fail set to warn to let unknown ages pass
summary true write the job summary
sarif also write SARIF to this path
config a config file other than the defaults

If you want the results as code scanning alerts on the lockfile lines, write SARIF and upload it (this needs security-events: write):

      - uses: TexWoW/unripe@v0.2.0
        with:
          sarif: unripe.sarif
      - uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: unripe.sarif

As a pre-commit hook

repos:
  - repo: https://github.com/TexWoW/unripe
    rev: v0.2.0
    hooks:
      - id: unripe

It runs when a lockfile changes. Add args: [--since, HEAD] if you only want it to look at what the commit adds.

Configuration

Put settings in .unripe.toml, or under [tool.unripe] in pyproject.toml. Flags on the command line win. Excludes from both are combined.

min-age = "3d"
unknown = "fail"
paths = ["web", "api"]
exclude = [
  "npm:@myorg/*",
  "left-pad@1.3.0",
]

An exclude is a package name or a glob. Prefix it with an ecosystem (npm:, pypi:, cargo:, go:, composer:, rubygems:) to limit it to one, and add @version to limit it to certain versions, like @types/node@20.*. PyPI names are normalised the way PyPI does it, so Typing_Extensions and typing-extensions are the same package.

Command line

unripe check [paths...]
  --min-age AGE         default 3d
  --since REF           only versions added since this git ref
  --exclude PATTERN     can be repeated
  --unknown fail|warn   default fail
  --format FORMAT       text, json, sarif or markdown
  --output FILE         write the report here instead of stdout
  --summary FILE        also append a markdown report, e.g. $GITHUB_STEP_SUMMARY
  --sarif FILE          also write a SARIF report
  --config FILE
  --no-cache
  --verbose             list the versions that passed too

unripe explain ECOSYSTEM NAME VERSION

unripe with no command runs unripe check. It exits 0 when everything is old enough, 1 when something is too new or has an unknown age, and 2 when a lockfile can't be read, no lockfile was found, or an option is wrong.

explain is handy for a single question:

$ unripe explain npm left-pad 1.3.0
npm left-pad 1.3.0: published 2018-04-09 01:10 UTC, 8y ago
old enough: the minimum age is 3d

Details worth knowing

If a registry can't tell unripe when a version was published, because it's down or rate limiting or has never heard of the version, that version counts as unknown, and unknown fails. A check that quietly passes when the registry is unreachable is one an attacker can turn off. Use --unknown warn if you'd rather not.

Publish times never change, so answers are cached on disk forever, in ~/.cache/unripe (%LOCALAPPDATA%\unripe\cache on Windows), or wherever UNRIPE_CACHE points. A second run on the same lockfiles makes no requests at all. Each version is looked up once even if several lockfiles have it, and lookups run 16 at a time. For npm, Cargo, Composer and RubyGems one request covers every version of a package.

Mirrors work through UNRIPE_NPM_REGISTRY and UNRIPE_PYPI_URL. For Go, unripe uses the first proxy in GOPROXY.

Go needed extra work. The module proxy only reports a version's commit time, and a commit time is whatever the author says it is, so a tag pushed today on an old commit (or a commit dated last year on purpose) would look old. So unripe also asks Go's checksum database, sum.golang.org, which gives every module version a number in the order it first saw them and can't be rewritten. Placing that number against the timeline of the Go module index says roughly when the version first appeared. If the commit claims to be older than that, unripe goes by first appearance. For recent versions the estimate is within minutes; for old ones, where it can't narrow things down, it uses the youngest age it can't rule out. GOSUMDB, GONOSUMDB and GOPRIVATE are honoured, and private modules fall back to the commit time.

uv.lock and composer.lock carry publish times themselves and unripe trusts them. Someone who can edit your lockfile can change those times, but they could also point it at any package they like, so that isn't something unripe tries to defend against.

Next to the built-in settings

As of October 2026:

Setting
npm 11.10+ min-release-age in .npmrc, in days
pnpm 10.16+ minimumReleaseAge in pnpm-workspace.yaml, in minutes (on by default since pnpm 11)
yarn 4.10+ npmMinimalAgeGate in .yarnrc.yml
bun 1.3+ minimumReleaseAge in bunfig.toml, in seconds
uv exclude-newer, a date or a duration like 7d
pip 26.1+ --uploaded-prior-to P3D
Bundler 4.0.13+ bundle config set cooldown 3, in days
Hex 2.5.0+ mix hex.config cooldown 3d
Cargo unstable -Zmin-publish-age now, expected stable in Rust 1.100
Composer proposed for 2.11
Go, NuGet, Maven, Gradle nothing yet

These stop a new version at install time, which unripe can't do, so use them where you can. unripe is the check in CI afterwards: one policy for the whole repo, run against what actually got locked. One more difference: some of the built-in settings let a version through when they can't find its publish date. unripe fails instead.

cooldowns.dev keeps a longer, up to date list of these settings, with examples.

Using it from Python

import unripe

for finding in unripe.scan(["."], min_age="3d", exclude=["npm:@myorg/*"]):
    if finding.state != "ok":
        print(finding.package, finding.state, finding.reason)

Each finding's state is ok, unripe, unknown or excluded. scan() raises unripe.check.Problem if a lockfile can't be read or the since ref doesn't exist.

Questions

Our own packages are always new. Exclude them, e.g. npm:@myorg/*.

We use a private registry. unripe can't see into it, so those packages come back unknown. Exclude them, or use --unknown warn. For private Go modules, point GOPROXY at your own proxy.

Does this replace npm audit or similar? No. Audit tools know about reported vulnerabilities. unripe doesn't know anything about any package. It just won't trust a version until it has been around for a while. They catch different things, so run both.

Is it slow? The first run on a big repo takes a while. Twenty real lockfiles with about 7,800 versions between them took 82 seconds cold and 1 second after that.

Contributing

Bug reports and pull requests are welcome. CONTRIBUTING.md explains how to run the tests and how to add a new ecosystem, which is the most useful thing anyone could add right now.

License

MIT

Metadata

Release files for unripe 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for unripe 0.2.0
File Size Uploaded Provenance
unripe-0.2.0.tar.gz 55.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for unripe 0.2.0
File Interpreter ABI Platform
unripe-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 90.9 kB

Release files / unripe-0.2.0.tar.gz

Download URL unripe-0.2.0.tar.gz
Size 55.1 kB
Tags Source
SHA-256 checksum
How to use checksums
bc9554e2e530ddbaf22ae1e5a4a0e34531b7e0412c60ab4e1f1818a4dd81bb49
BLAKE2b-256 checksum
How to use checksums
5bcbb155329d0f0eabfd09d27ebdb503196a6ee59c623d1f359101eceb2aa404
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / unripe-0.2.0-py3-none-any.whl

Download URL unripe-0.2.0-py3-none-any.whl
Size 35.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d17488ca6eb8969f7e7c0908501663ecacf26fd7b8b02e049ea93f0d8eb386f0
BLAKE2b-256 checksum
How to use checksums
0d1d6055e52e8482b36e67988ec1d39dc988ce044d5a6c2945085a020f9cb2cf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for unripe 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for unripe-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page