# How to Verify Dependencies with Hashes in uv


Dependency hashes let [uv](https://pydevtools.com/handbook/reference/uv.md) 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](https://pydevtools.com/handbook/explanation/what-is-a-python-supply-chain-attack.md).

## 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:

```toml
[[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](https://pydevtools.com/handbook/how-to/how-to-export-requirements-txt-from-a-uv-project.md) to keep the locked versions. `uv export` includes hashes by default (pass `--no-hashes` to omit them):

```bash
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:

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

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

```text
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:

```bash
uv pip install --require-hashes -r requirements.txt
```

```text
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:

```text
error: In `--require-hashes` mode, all requirements must have a hash, but none were provided for: six==1.17.0
```

```text
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:

```toml
[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:

```text
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`](https://pydevtools.com/handbook/how-to/how-to-protect-against-python-supply-chain-attacks-with-uv.md) keeps freshly published versions out, a [lockfile](https://pydevtools.com/handbook/how-to/how-to-use-a-uv-lockfile-for-reproducible-python-environments.md) pins exact versions, and [digital attestations](https://pydevtools.com/handbook/how-to/how-to-publish-python-packages-with-digital-attestations.md) tie a PyPI file back to the build that produced it.
