[agent] Found by the scheduled npm bug-hunt routine (ledger #302). The Bun routine noticed it first and handed it over (ledger entry entries/npm/20261003T193043Z-from-bun.md).
Summary
#454 ("scan --sync / --mode agent never re-applies an already-recorded patch") was fixed by #456 in the shared fetch loop: patches already recorded in .socket/manifest.json now count towards the nested apply (get.rs to_apply = downloaded + batch.already_recorded). But the human-output scan path drops those selections before they reach that loop. crates/socket-patch-cli/src/commands/scan/mod.rs:2733-2771 partitions every selection whose uuid is already recorded into already_recorded, prints [skip] … (already recorded: …), and when nothing else is left returns finish_human(0) without applying anything. That partition came in with de316b4 (#358), merged six minutes before the #456 fix, so #454 still reproduces whenever --json is omitted.
So the same scan --mode agent (or scan --sync) command patches the files with --json and leaves them unpatched without it. get <purl> --mode agent (human) does re-apply.
Impact
After any reinstall (npm ci, rm -rf node_modules && npm install, a CI cache miss), a human-output socket-patch scan --mode agent or scan --sync exits 0 and leaves the vulnerable bytes installed. It prints "All selected patches are already recorded in the manifest; run socket-patch apply to re-apply them.", but a CI log or a cron job only sees exit 0. vex afterwards refuses (not_applied), so attestation stays honest. Only the disk state is wrong.
Repro (npm, Linux, main 045d7ec)
A local mock of the patch API serves one free patch for left-pad@1.3.0, prepending /* SOCKET-PATCHED */ to index.js.
mkdir p && cd p && git init -q
echo '{"name":"p","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
npm install
socket-patch scan --mode agent --yes # patched
rm -rf node_modules && npm ci # pristine bytes again
socket-patch scan --mode agent --yes # or: scan --sync --yes
# [skip] pkg:npm/left-pad@1.3.0 (already recorded: 11111111)
# All selected patches are already recorded in the manifest; run `socket-patch apply` to re-apply them.
# exit 0, node_modules/left-pad/index.js still unpatched
socket-patch scan --mode agent --yes --json # apply.applied: 1, file patched
Expected vs actual
Cells
| OS |
npm / Node |
human scan --mode agent after npm ci |
human scan --sync |
--json |
| Linux |
6.14.18 / 22.22 |
unpatched, exit 0 |
— |
re-applied |
| Linux |
10.9.4 / 22.22 |
unpatched, exit 0 (×3) |
unpatched, exit 0 |
re-applied |
| Linux |
12.2.0 / 24 |
unpatched, exit 0 |
— |
re-applied |
| Linux (Bun 1.4.2, from the handover) |
— |
unchanged |
— |
re-applied |
The decision is in OS-independent code, so I didn't run a macOS / Windows probe.
First bad version
Suspect code
crates/socket-patch-cli/src/commands/scan/mod.rs:2733-2771: the already_recorded partition and the early finish_human(0).
[agent] Found by the scheduled npm bug-hunt routine (ledger #302). The Bun routine noticed it first and handed it over (ledger entry
entries/npm/20261003T193043Z-from-bun.md).Summary
#454 ("scan --sync / --mode agent never re-applies an already-recorded patch") was fixed by #456 in the shared fetch loop: patches already recorded in
.socket/manifest.jsonnow count towards the nested apply (get.rsto_apply = downloaded + batch.already_recorded). But the human-output scan path drops those selections before they reach that loop.crates/socket-patch-cli/src/commands/scan/mod.rs:2733-2771partitions every selection whose uuid is already recorded intoalready_recorded, prints[skip] … (already recorded: …), and when nothing else is left returnsfinish_human(0)without applying anything. That partition came in with de316b4 (#358), merged six minutes before the #456 fix, so #454 still reproduces whenever--jsonis omitted.So the same
scan --mode agent(orscan --sync) command patches the files with--jsonand leaves them unpatched without it.get <purl> --mode agent(human) does re-apply.Impact
After any reinstall (
npm ci,rm -rf node_modules && npm install, a CI cache miss), a human-outputsocket-patch scan --mode agentorscan --syncexits 0 and leaves the vulnerable bytes installed. It prints "All selected patches are already recorded in the manifest; runsocket-patch applyto re-apply them.", but a CI log or a cron job only sees exit 0.vexafterwards refuses (not_applied), so attestation stays honest. Only the disk state is wrong.Repro (npm, Linux, main
045d7ec)A local mock of the patch API serves one free patch for
left-pad@1.3.0, prepending/* SOCKET-PATCHED */toindex.js.Expected vs actual
get.rs:2441-2447), and the output format doesn't change what gets written. The--synchelp (scan/mod.rs:281-283) presents it as the one-shot reconciliation.selectedno longer contains it.Cells
scan --mode agentafternpm ciscan --sync--jsonThe decision is in OS-independent code, so I didn't run a macOS / Windows probe.
First bad version
--jsonfixed, human still broken. The human skip itself is de316b4 (Keep Composer redirects and rollback consistent #358). Its testscan_human_does_not_offer_an_already_recorded_patch(tests/covgap_commands_scan_mod.rs:2265) says the selection "would only be downloaded to be skipped", which stopped being true once Fix agent scan/get skipping apply for recorded patches (#454) #456 landed.Suspect code
crates/socket-patch-cli/src/commands/scan/mod.rs:2733-2771: thealready_recordedpartition and the earlyfinish_human(0).