Skip to content

Hatch 1.17+ projects with a hatch-generated pylock.toml get only the lock rewritten, and Hatch regenerates it from pyproject, so hosted and vendored patches are silently dropped #479

Description

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

Summary

Hatch 1.17.0 added environment locking (hatch lock / hatch dep lock, with locked = true or lock-envs = true). It writes a PEP 751 pylock.toml (or pylock.<env>.toml) that Hatch derives from pyproject. When that file is present, scan --mode hosted and scan --mode vendored rewrite only pylock*.toml and leave pyproject.toml alone. Both report success.

Hatch never treats that lock as the source of truth:

  • In an existing env, the dependency hash (computed from pyproject) hasn't changed, so Hatch doesn't sync and the upstream six stays installed.
  • In a fresh env (any CI checkout, or hatch env remove), Hatch sees no stored hash, runs generate_lockfile (hatch/project/core.py: if not lockfile_path.is_file() or new_dep_hash != current_dep_hash), and overwrites the rewritten pylock.toml from pyproject. The Socket URL or .socket/vendor path is thrown away, and the env installs the upstream PyPI wheel.
  • hatch dep lock --check fails (exit 1) right after the scan, so a CI lock-freshness gate breaks too.

In vendored mode the same route also skips the documented refusal ("Vendored Hatch requires the pip installer"): these envs use installer = "uv", because Hatch's pip locker can't apply locks. Without a pylock, the same project is refused (partial_failure). With one, vendored scan reports applied: 1.

The same thing happens with a pylock.toml exported for reference by hatch dep lock --export pylock.toml on a non-locked env. Only the lock is rewritten, and Hatch installs from pyproject, so nothing is patched.

Control: the same locked project with no committed pylock works. socket-patch rewrites pyproject (six @ <url>#sha256=… plus allow-direct-references), Hatch's re-lock carries the URL into pylock.toml, hatch dep lock --check exits 0, and a fresh env imports the patched six. Rewriting pyproject is all that's needed. The pylock-only route is what drops the patch.

Impact

Any Hatch ≥ 1.17 project that commits its lock gets a scan that says success but never produces a patched install, on any machine, in either mode. Hatch also rewrites the lock back to upstream on the next fresh env, so the wiring disappears from the working tree as well. vex attests not_affected / inline_mitigations_already_exist from the rewritten lock while no Hatch env has the patched bytes.

Repro (Linux; mock patch API as in #335, SOCKET_PATCH_SERVER_URL set to the mock)

SP="socket-patch --api-url $API --api-token t --org test-org --patch-server-url $API --ecosystems pypi"
mkdir -p app/src/app && cd 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]
locked = true
installer = "uv"
EOF
hatch lock                                    # writes pylock.toml (created-by = "uv")
hatch run python -c 'import six'              # env exists, upstream six
$SP scan --mode hosted --yes --json           # success, rewrittenFiles: ["pylock.toml"], warnings: []
git diff --stat                               # only pylock.toml; pyproject untouched
hatch run python -c 'import six;print(hasattr(six,"SOCKET_PATCHED"))'   # False (existing env)
hatch dep lock --check; echo $?               # 1
HATCH_DATA_DIR=$(mktemp -d) hatch run python -c 'import six;print(hasattr(six,"SOCKET_PATCHED"))'   # False (fresh env)
grep -c 127.0.0.1 pylock.toml                 # 0: Hatch regenerated the lock from pyproject

Vendored: the same, but with scan --mode vendored --yes --json → success, applied: 1, pylock archive = { path = ".socket/vendor/pypi/…" }; the fresh env is unpatched and the lock is regenerated without the path.

Expected vs actual

  • Expected: docs/testing/hatch.md says that Hatch 1.x projects are wired through exact PEP 508 declarations in project.dependencies and the env dependencies, and that vendored Hatch "requires the pip installer". CLI_CONTRACT.md lists "PEP 508 direct references in pyproject.toml / hatch.toml" as the Hatch hosted/vendored surface. A Hatch-generated pylock*.toml is derived from pyproject, so pyproject has to be rewritten, with the lock either updated alongside it or left for Hatch to regenerate. Otherwise socket-patch should refuse or warn loudly. Exiting success with nothing installable is wrong.
  • Actual: only pylock*.toml is rewritten, success, no warnings. Existing and fresh Hatch envs stay unpatched, the next fresh env reverts the lock, hatch dep lock --check fails, and vendored skips the uv-installer refusal.

OS × Hatch matrix (main 6e7ef74)

Hatch 1.16.5 Hatch 1.17.0 Hatch 1.18.1
Linux hosted n/a (no hatch lock) fail fail (default env and pylock.test.toml for a named env; reproduced 3×)
Linux vendored n/a fail fail (2×)
macOS hosted / vendored n/a fail (vendored seen in the log) fail / fail
Windows hosted / vendored n/a fail (vendored seen in the log) fail / fail
Linux, no committed pylock (control) — — pass (pyproject rewritten, fresh env patched, --check 0)

installer = "pip" (pip locker) is out of scope: Hatch itself refuses to apply pip-locker lockfiles (LockerUnsupportedError).

Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36887192239 (all 3 OS jobs; the per-cell lines are printed in the "Probe Hatch locked envs" step).

First bad

This has been present since Hatch support landed (#244), and was exposed by Hatch 1.17.0's lock feature. Released v4.0.0 has no Hatch lane (redirected: 0 on this project).

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:686-695 (rewrite_hatch): it returns early when any uv.lock / poetry.lock / pdm.lock / Pipfile.lock exists or is_python_lock_name(file) matches, which includes pylock.toml / pylock.<name>.toml. A Hatch-owned pylock (created-by = "uv" next to [tool.hatch.envs.*] locked = true, tool.hatch.lock-envs, or a matching lock-filename) isn't an independent install source.
  • The vendored uv-installer refusal (crates/socket-patch-core/src/utils/hatch.rs:372-380, vendor/pypi_hatch.rs:44) never runs on this route, because the pylock lane wins first.

Related, separate: #335 (no Hatch-env stale-install detection), and #407 (hosted rollback of a uv pip compile pylock, which is what Hatch's uv locker writes).

Activity

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