Safeguard
Developer Tools•

Which uv index strategy is safe with an internal Python index?

uv defaults to first-index, which blocks dependency confusion. What the unsafe options do, and how explicit indexes and pins keep an internal index safe.

Safeguard Security Team
4 min read

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:

StrategyBehaviorRisk
first-index (default)Searches indexes in order and limits candidates to the first index that has the packageLowest
unsafe-first-matchPrefers the first index with a compatible version, even if others have newer onesHigher
unsafe-best-matchPools versions from all indexes and picks the bestClosest 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:

  1. Mark an index as explicit. Its packages are not installed from it unless you pin them to it.
  2. 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

  1. Search your repository and CI files for the strategy option or its environment variable.
  2. If you find one of the unsafe values, find out why. Some projects set it to resolve a version conflict and forget it.
  3. List every internal package name and confirm each is pinned to the internal index.
  4. 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.