Skip to content

How to Verify Dependencies with Hashes in uv

Dependency hashes let uv verify that every downloaded artifact matches what was resolved, catching a swapped or corrupted file before anything gets installed. This is one layer of defense against a Python supply chain attack.

Read the hashes in uv.lock

uv.lock records a SHA-256 hash for every distribution by default. After uv lock or uv add, each entry lists hashes for the sdist and every wheel:

[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "...", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" }
wheels = [
    { url = "...", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" },
]

uv sync and uv run check each artifact against these hashes when they fetch or install it. Verification happens only at that moment: rerunning uv sync on an environment that already matches the lockfile checks nothing, because nothing is downloaded. uv sync and uv lock have no hash flags; hashes are always recorded and always verified.

Generate a requirements.txt with hashes

Some deployment workflows use requirements.txt instead of uv.lock. Two commands produce requirements files with hashes.

Export from a uv project to keep the locked versions. uv export includes hashes by default (pass --no-hashes to omit them):

uv export --locked --no-emit-project -o requirements.txt

--locked fails if the lockfile needs updating. --no-emit-project omits the root project, whose editable entry cannot be installed in hash-checking mode.

uv pip compile needs the --generate-hashes flag:

uv pip compile pyproject.toml --generate-hashes -o requirements.txt

Either way, each requirement carries one --hash per acceptable file:

six==1.17.0 \
    --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
    --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81

Require hashes with uv pip install

uv pip install verifies any hashes present in a requirements file but does not insist that every requirement has one. Pass --require-hashes to make missing hashes an error:

uv pip install --require-hashes -r requirements.txt
Resolved 1 package in 1ms
Installed 1 package in 1ms
 + six==1.17.0

For a requirement specified by package name and version, omitting its hash or exact == pin produces these errors. A direct URL with a matching hash is also accepted:

error: In `--require-hashes` mode, all requirements must have a hash, but none were provided for: six==1.17.0
error: In `--require-hashes` mode, all requirements must have their versions pinned with `==`, but found: six

Hash-checking mode rejects Git dependencies, editable installs, and local directories (a local .whl or .tar.gz is fine). To make it the default for a project, set it in pyproject.toml; the UV_REQUIRE_HASHES environment variable does the same in CI:

[tool.uv.pip]
require-hashes = true

Recognize a hash mismatch

When a downloaded file matches none of the listed hashes, uv aborts with the expected and computed digests:

Resolved 1 package in 71ms
  × Failed to download `six==1.17.0`
  ╰─▶ Hash mismatch for `six==1.17.0`

      Expected:
        sha256:0000f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274
        sha256:0000335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81

      Computed:
        sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274

Treat this as a stop sign. Regenerate the requirements file from a trusted machine and compare the hashes with the ones PyPI publishes before installing anything.

To install anyway (for example, while diagnosing a corrupt internal mirror), pass --no-verify-hashes or set verify-hashes = false under [tool.uv.pip]. Both skip verification entirely, so keep them out of committed configuration.

Know what hashes cover

The listed hashes are an allow-list of acceptable files, not a check on any particular file. uv only considers files whose hash appears in the list. If a requirement lists a wheel hash and an sdist hash and only the wheel’s hash is wrong, uv skips the wheel and builds from the sdist, whose hash still matches, and the install succeeds with no warning. Pair hashes with --no-build when a silent fallback to a source build is not acceptable.

Important

Hashes do not protect against a malicious package uploaded directly to PyPI. If an attacker publishes a compromised version through legitimate channels, the hashes match because they were generated from the compromised artifact itself.

For that threat, layer the other defenses: a dependency cooldown with --exclude-newer keeps freshly published versions out, a lockfile pins exact versions, and digital attestations tie a PyPI file back to the build that produced it.

Last updated on