Keep the default, first-index. With it, uv stops at the first index that contains a package, so a public package cannot override an internal one of the same name. The two unsafe strategies pool or reorder candidates across indexes, and uv's own docs warn that one of them exposes you to dependency confusion.
What the three strategies do
uv lets you set the strategy with a command option or an environment variable. The docs describe three values:
| Strategy | Behavior | Risk |
|---|---|---|
| first-index (default) | Searches indexes in order and limits candidates to the first index that has the package | Lowest |
| unsafe-first-match | Prefers the first index with a compatible version, even if others have newer ones | Higher |
| unsafe-best-match | Pools versions from all indexes and picks the best | Closest to pip, exposes you to dependency confusion per the docs |
The word unsafe is in the names on purpose. If you need your internal package and a public package of the same name never to mix, the default is the one built for that.
Why this matters for a mirror setup
A common small-team setup adds an internal index next to the public one. Under a pooling strategy, an attacker who publishes a higher version of your internal package name on the public index can win the version comparison. Under first-index, the internal index answers first and the public one is never consulted for that name.
Make the setup explicit
Two more controls from the uv docs tighten things further:
- Mark an index as explicit. Its packages are not installed from it unless you pin them to it.
- Pin a package to an index in the sources table of pyproject.toml. Named indexes used there must be defined in the same pyproject.toml.
[[tool.uv.index]]
name = "internal"
url = "https://pypi.example.internal/simple"
explicit = true
[tool.uv.sources]
acme-core = { index = "internal" }
With this shape, acme-core can come only from the internal index, and nothing else is ever fetched from it by accident.
Also note how the default index is treated. PyPI is the default, and the docs say the default index is always treated as lowest priority regardless of its position in the list. If you want your mirror to replace PyPI, mark it as default, and PyPI stops being the default.
A review you can do in ten minutes
- Search your repository and CI files for the strategy option or its environment variable.
- If you find one of the unsafe values, find out why. Some projects set it to resolve a version conflict and forget it.
- List every internal package name and confirm each is pinned to the internal index.
- Commit the lockfile and fail CI if it changes unexpectedly.
Where Safeguard fits
Safeguard's SBOM and dependency scanning shows the resolved Python packages in a project, which helps you confirm that internal names resolve to the versions you expect. Try a free scan at gold.safeguard.sh/scan.
Try it yourself
Click through it at your own pace, or open it full screen.
Scan the package list for names that match your internal prefix and check the version shown against what you published.
Quick check
Is unsafe-best-match ever fine?
Only when you trust every index in the list equally. For mixed public and internal indexes, avoid it.
Does an explicit index stop installs from it entirely?
No. Packages pinned to that index still install from it. Others are not taken from it.
Sources
Last reviewed: 19 October 2026.