[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).
[agent] Found by the scheduled Hatch bug-hunt routine (ledger #314).
Summary
Hatch 1.17.0 added environment locking (
hatch lock/hatch dep lock, withlocked = trueorlock-envs = true). It writes a PEP 751pylock.toml(orpylock.<env>.toml) that Hatch derives from pyproject. When that file is present,scan --mode hostedandscan --mode vendoredrewrite onlypylock*.tomland leavepyproject.tomlalone. Both reportsuccess.Hatch never treats that lock as the source of truth:
hatch env remove), Hatch sees no stored hash, runsgenerate_lockfile(hatch/project/core.py:if not lockfile_path.is_file() or new_dep_hash != current_dep_hash), and overwrites the rewrittenpylock.tomlfrom pyproject. The Socket URL or.socket/vendorpath is thrown away, and the env installs the upstream PyPI wheel.hatch dep lock --checkfails (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 reportsapplied: 1.The same thing happens with a
pylock.tomlexported for reference byhatch dep lock --export pylock.tomlon 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=…plusallow-direct-references), Hatch's re-lock carries the URL intopylock.toml,hatch dep lock --checkexits 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
successbut 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.vexattestsnot_affected/inline_mitigations_already_existfrom the rewritten lock while no Hatch env has the patched bytes.Repro (Linux; mock patch API as in #335,
SOCKET_PATCH_SERVER_URLset to the mock)Vendored: the same, but with
scan --mode vendored --yes --json→success,applied: 1, pylockarchive = { path = ".socket/vendor/pypi/…" }; the fresh env is unpatched and the lock is regenerated without the path.Expected vs actual
project.dependenciesand the envdependencies, and that vendored Hatch "requires the pip installer". CLI_CONTRACT.md lists "PEP 508 direct references inpyproject.toml/hatch.toml" as the Hatch hosted/vendored surface. A Hatch-generatedpylock*.tomlis 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. Exitingsuccesswith nothing installable is wrong.pylock*.tomlis rewritten,success, no warnings. Existing and fresh Hatch envs stay unpatched, the next fresh env reverts the lock,hatch dep lock --checkfails, and vendored skips the uv-installer refusal.OS × Hatch matrix (main
6e7ef74)hatch lock)pylock.test.tomlfor a named env; reproduced 3×)--check0)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: 0on this project).Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:686-695(rewrite_hatch): it returns early when anyuv.lock/poetry.lock/pdm.lock/Pipfile.lockexists oris_python_lock_name(file)matches, which includespylock.toml/pylock.<name>.toml. A Hatch-owned pylock (created-by = "uv"next to[tool.hatch.envs.*] locked = true,tool.hatch.lock-envs, or a matchinglock-filename) isn't an independent install source.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 compilepylock, which is what Hatch's uv locker writes).