[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).
Summary
A package is vendored in a Pipenv named category (for example [docs]), and it's the only package in that category. When the user runs pipenv uninstall six --categories docs, Pipenv 2022.x and 2023.x drop the now-empty docs key from Pipfile.lock entirely. Pipenv 2026 keeps "docs": {}.
On main, the vendored Pipenv revert reads a missing category key as drift, not as removal. So after the uninstall:
vendor --check exits 1. It says "dependency removed … run socket-patch scan --mode vendored --prune to revert the vendored entry".
scan --mode vendored --prune exits 0 but lists the entry under gc.keptVendoredEntries. The artifact and the ledger entry stay.
vendor --check is still red afterwards.
vendor --revert exits 0 and keeps the entry: "undo the drift (restore the vendored lock entries or re-vendor) and re-run vendor --revert".
remove pkg:pypi/six@1.16.0 and rollback pkg:pypi/six@1.16.0 exit 1: "re-run scan --mode vendored to normalize, then remove again". A re-scan is a no-op, because six is no longer in the project.
The user can't "undo the drift" without reinstalling the package they just removed. The only way to get vendor --check green again is to delete .socket/vendor/state.json entries and the artifact by hand.
Impact
Once a dependency is uninstalled this way, a CI gate on vendor --check stays red permanently. Every remedy the CLI prints is a no-op or loops. The dead wheel and ledger entry are never reclaimed. Nothing installs unpatched bytes, so this isn't a security issue, but the CI gate is broken.
Expected vs actual
- Expected: the revert already retires a record whose entry a relock dropped from a category that still exists. It returns
vendor_lock_entry_relocked and says so in its comment: "A relock dropped the entry (pipenv uninstall <pkg>, a Pipfile edit + pipenv lock) … retire the record rather than keep the orphan forever" (crates/socket-patch-core/src/vendor/pypi_pipenv.rs:616-626). docs/usage.md:223 says --prune removes "records for dependencies that left the project". vendor --check itself sends the user to scan --prune. When the uninstall also removed the category key, the record should be retired in the same way.
- Actual: with the whole category gone, the record is treated as drift and kept forever.
Repro (Linux, main 823810a, real Pipenv 2023.12.1 on py3.11, local mock patch API serving a patched six 1.16.0 wheel)
mkdir p && cd p && git init -q
cat > Pipfile <<'EOF'
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
idna = "==3.7"
[docs]
six = "==1.16.0"
[requires]
python_version = "3.11"
EOF
pipenv lock
socket-patch scan --mode vendored --yes $MOCK_ARGS # docs:six -> ./.socket/vendor/pypi/<uuid>/six-…whl
git add -A && git commit -qm vendored
pipenv uninstall six --categories docs # Pipfile keeps an empty [docs]; Pipfile.lock loses the "docs" key
socket-patch vendor --check; echo $? # 1: "dependency removed … run `scan --mode vendored --prune`"
socket-patch scan --mode vendored --yes --prune $MOCK_ARGS --json | jq .gc.keptVendoredEntries # ["pkg:pypi/six@1.16.0"]
socket-patch vendor --check; echo $? # still 1
socket-patch vendor --revert # "Pipfile.lock entry for Some(\"docs:six\") changed since vendoring; left untouched" … Kept 1 drifted package
socket-patch remove pkg:pypi/six@1.16.0 --yes; echo $? # 1: drift-kept, "re-run scan --mode vendored to normalize"
The same shape fails if [docs] is edited by hand and then pipenv lock runs. Control: when six is removed from [docs] on Pipenv 2026.8.0 (which keeps "docs": {}), or from [packages] on any version (default always stays), scan --prune reverts the entry, the artifact is removed and vendor --check exits 0.
OS × version
| OS |
Pipenv |
result |
| Linux |
2022.12.19 |
fail (2/2 runs) |
| Linux |
2023.12.1 |
fail (2/2 runs; remove / rollback also exit 1) |
| Linux |
2026.8.0 |
pass (Pipenv keeps the empty docs key) |
| macOS / Windows |
— |
untested (lock-shape bug, OS-independent) |
First bad: not in v4.0.0, which has no vendor --check, and whose scan --prune never judged Pipenv entries. On main, #1050 routes the prune GC and the check's "dependency removed" remedy through Discovery::vendor_entry_in_use. That verdict correctly returns Some(false) here, but the revert then drift-keeps the entry.
Suspect code
crates/socket-patch-core/src/vendor/pypi_pipenv.rs:600-602 (revert_pipenv): let Some(map) = lock.get_mut(section)… else { warnings.push(drifted()); continue; }. A missing category is reported as vendor_lock_entry_drifted. The missing-entry arm a few lines below (:616-626) retires a Rewritten record that has an original. The missing-section case could take the same arm, perhaps only when the section isn't _meta and the lock still parses as a pipfile-spec 6 lock. The unit test revert_drift_skips_missing_section_entry_new_and_missing_original (:1804) currently pins the drift behaviour for a deleted section.
Related, but in other backends with separate code: #1132 (bun.lockb) and #1140 (uv).
Probe runs: none. This is a lock-shape bug that doesn't depend on the OS, and probe branches are currently blocked for this routine.
[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).
Summary
A package is vendored in a Pipenv named category (for example
[docs]), and it's the only package in that category. When the user runspipenv uninstall six --categories docs, Pipenv 2022.x and 2023.x drop the now-emptydocskey fromPipfile.lockentirely. Pipenv 2026 keeps"docs": {}.On main, the vendored Pipenv revert reads a missing category key as drift, not as removal. So after the uninstall:
vendor --checkexits 1. It says "dependency removed … runsocket-patch scan --mode vendored --pruneto revert the vendored entry".scan --mode vendored --pruneexits 0 but lists the entry undergc.keptVendoredEntries. The artifact and the ledger entry stay.vendor --checkis still red afterwards.vendor --revertexits 0 and keeps the entry: "undo the drift (restore the vendored lock entries or re-vendor) and re-runvendor --revert".remove pkg:pypi/six@1.16.0androllback pkg:pypi/six@1.16.0exit 1: "re-runscan --mode vendoredto normalize, then remove again". A re-scan is a no-op, because six is no longer in the project.The user can't "undo the drift" without reinstalling the package they just removed. The only way to get
vendor --checkgreen again is to delete.socket/vendor/state.jsonentries and the artifact by hand.Impact
Once a dependency is uninstalled this way, a CI gate on
vendor --checkstays red permanently. Every remedy the CLI prints is a no-op or loops. The dead wheel and ledger entry are never reclaimed. Nothing installs unpatched bytes, so this isn't a security issue, but the CI gate is broken.Expected vs actual
vendor_lock_entry_relockedand says so in its comment: "A relock dropped the entry (pipenv uninstall <pkg>, a Pipfile edit +pipenv lock) … retire the record rather than keep the orphan forever" (crates/socket-patch-core/src/vendor/pypi_pipenv.rs:616-626).docs/usage.md:223says--pruneremoves "records for dependencies that left the project".vendor --checkitself sends the user toscan --prune. When the uninstall also removed the category key, the record should be retired in the same way.Repro (Linux, main
823810a, real Pipenv 2023.12.1 on py3.11, local mock patch API serving a patchedsix 1.16.0wheel)The same shape fails if
[docs]is edited by hand and thenpipenv lockruns. Control: when six is removed from[docs]on Pipenv 2026.8.0 (which keeps"docs": {}), or from[packages]on any version (defaultalways stays),scan --prunereverts the entry, the artifact is removed andvendor --checkexits 0.OS × version
remove/rollbackalso exit 1)docskey)First bad: not in v4.0.0, which has no
vendor --check, and whosescan --prunenever judged Pipenv entries. On main, #1050 routes the prune GC and the check's "dependency removed" remedy throughDiscovery::vendor_entry_in_use. That verdict correctly returnsSome(false)here, but the revert then drift-keeps the entry.Suspect code
crates/socket-patch-core/src/vendor/pypi_pipenv.rs:600-602(revert_pipenv):let Some(map) = lock.get_mut(section)… else { warnings.push(drifted()); continue; }. A missing category is reported asvendor_lock_entry_drifted. The missing-entry arm a few lines below (:616-626) retires aRewrittenrecord that has anoriginal. The missing-section case could take the same arm, perhaps only when the section isn't_metaand the lock still parses as a pipfile-spec 6 lock. The unit testrevert_drift_skips_missing_section_entry_new_and_missing_original(:1804) currently pins the drift behaviour for a deleted section.Related, but in other backends with separate code: #1132 (bun.lockb) and #1140 (uv).
Probe runs: none. This is a lock-shape bug that doesn't depend on the OS, and probe branches are currently blocked for this routine.