Skip to content

Hosted Hatch scan exits 0 "redirected" when [tool.uv.sources] pins the patched package, but Hatch's uv installer obeys the source: url/path sources install the unpatched wheel and git sources break every install #673

Description

[agent] Found by the scheduled Hatch bug-hunt routine (ledger #314).

Summary

In a Hatch project whose environment uses installer = "uv", a [tool.uv.sources] entry for the patched package (for example a url, path or git source) takes precedence over the PEP 508 direct reference that hosted mode writes into [project].dependencies. Hatch installs the project with uv pip install -e ., and uv applies the project's tool.uv.sources. Hosted scan never looks at [tool.uv.sources]. It rewrites six==1.16.0 to six @ <patch url>#sha256=… and reports success, redirected: 1, exit 0. Then:

  • with a url or path source, every fresh Hatch env silently installs the unpatched upstream wheel;
  • with a git source, every Hatch env creation fails with Conflicting URLs for package six.

The docs say a source table is refused before anything is written, so the scan should have failed closed.

Impact

CI gets a green scan and a pyproject that looks patched, but the vulnerable code still ships. For git sources the project stops installing at all. vex does correctly omit six (not_applied) when it inspects the env, so the silent part is the scan result. Vendored mode on the same project refuses (with pypi_uv_no_lockfile), so it fails closed.

Repro (Hatch 1.18.1, uv 0.12, Linux; local mock patch API serving a patched six 1.16.0 wheel)

mkdir app && cd app && mkdir -p src/app && touch src/app/__init__.py
cat > pyproject.toml <<'EOF'
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "app"
version = "0.1.0"
dependencies = ["six==1.16.0"]

[tool.hatch.envs.default]
installer = "uv"

[tool.uv.sources]
six = { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl" }
EOF
socket-patch scan --mode hosted --yes --json --ecosystems pypi …   # exit 0, status success, redirect.redirected = 1, no warnings
grep dependencies pyproject.toml   # six @ http://…/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl#sha256=…
hatch run python -c 'import six; print(getattr(six, "SOCKET_PATCHED", "UNPATCHED"))'   # fresh env -> UNPATCHED

Swap the source for { path = "/abs/six-1.16.0-py2.py3-none-any.whl" } to get the same result. With { git = "https://github.com/benjaminp/six", tag = "1.16.0" }, hatch run fails with:

error: Requirements contain conflicting URLs for package `six`:
- git+https://github.com/benjaminp/six@1.16.0
- http://127.0.0.1:18080/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl

Expected vs actual

  • Expected: docs/testing/hatch.md: "A range, transitive-only declaration, dynamic dependency metadata, custom environment plugin, source table or conditional override is refused before writing. Use agent mode…". A uv source for a package Hatch installs through uv should be refused (or at least warned about) the same way Hatch sources are, with nothing written and a non-zero or warning result.
  • Actual: exit 0 success, redirected: 1, no warning. The rewrite is either ignored (url/path) or breaks installs (git).

Matrix (Linux, each cell run twice)

Shape Hatch 1.16.5 Hatch 1.18.1
installer = "uv", project deps, tool.uv.sources url fail (unpatched) fail (unpatched)
installer = "uv", project deps, tool.uv.sources path fail (unpatched) fail (unpatched)
installer = "uv", project deps, tool.uv.sources git fail (install error) fail (install error)
installer = "uv", features = ["x"] (optional deps), url source — fail (unpatched)
installer = "uv", env dependencies, url source — pass (env reqs are passed to uv as plain strings, so project sources don't apply)
installer = "uv", tool.uv.sources index source (explicit index) — pass (the direct URL wins)
pip installer (default), url source — pass (pip ignores tool.uv.sources)

macOS / Windows: not probed (probe branches can't be deleted from this sandbox right now). The rewrite is OS-independent.

Suspect code

crates/socket-patch-core/src/utils/hatch.rs:247 refuses only Hatch's own tool.hatch.sources / env. plan() (utils/hatch.rs:344) reads installer == "uv" for the env but never consults tool.uv.sources, so rewrite_project (utils/hatch.rs:187) rewrites a dependency that uv then resolves from the source. A fix could refuse (or warn and skip) any patched name that appears in [tool.uv.sources] when an env that installs the project uses the uv installer.

Related but different: #564 (uv lane, no-sources = true).

Activity

mikolalysenko commented on Oct 3, 2026

@mikolalysenko
CollaboratorAuthor

[agent] Triage: priority:p1 (Hatch, PyPI family). Confirmed on main 045d7ec: crates/socket-patch-core/src/utils/hatch.rs:247 refuses only Hatch's own sources/env tables, and nothing in hatch.rs reads [tool.uv.sources]. So hosted mode rewrites a dependency that the uv installer then resolves from the source. I found no duplicate. #564 (the uv lane's no-sources) is related but has a different code path. This isn't fixed on main and no PR is open for it.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions