Repository navigation
A bare scan or get on a project holding both vendored packages and hosted pins silently converts every hosted pin to vendored (exit 0, no warning), so it undoes a partial hosted takeover #1437
Description
[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
Since #1317 (the fix for #1088), a bare scan or get with no --mode picks its mode from the project's state (mode_from_project_state). That function only looks at the vendor ledger and the manifest. Hosted pins aren't counted, because hosted mode keeps no ledger. So a project that holds both vendored packages and hosted pins is classed as plain "vendored". The bare run then does a hosted → vendored takeover of every hosted pin: it restores the registry entry, commits a new .socket/vendor/npm/<uuid>/*.tgz, and adds a ledger entry. It exits 0 with status: success and no warning code in --json. Human output does admit it: Note: pkg:npm/minimist@1.2.2 was hosted; restored its upstream registry entry (bun.lock) before vendoring (mode takeover).
The CLI contract says the opposite (CLI_CONTRACT.md, "Mode resolution"): "Switching modes is always explicit: … the hosted → vendored takeover only under --mode vendored, and a bare scan/get never converts a project." docs/migrating-to-v5.md and docs/usage.md say the same.
Impact
socket-patch creates this mixed state itself, in two documented ways:
- A partial vendored → hosted takeover (Make the vendored-to-hosted takeover atomic #1039).
scan --mode hostedon a vendored project, where one purl can't be hosted (grantpending_build, denied, or no sha512), keeps that purl vendored and moves the rest to hosted. The run exits 0 and reportsredirect_takeover_reverted_vendored. The next barescan(the quick-start command, and what a CI job typically runs) silently undoes the takeover the user explicitly asked for: every hosted package goes back to vendored. - Per-package modes.
get <pkg> --mode vendoredplusget <other> --mode hosted(ledger run 25 covers these mixed per-package modes). Any later barescanor bareget <other>converts the hosted package to vendored.
The bytes stay patched, so this isn't a security hole. But it's an unrequested mode switch, the mirror image of #1088. It adds committed tarballs (repo size, review noise, the #831 / #1116 .gitignore hazards), changes what the lockfile resolves, and gives --json consumers no signal. It isn't specific to Bun: the same thing happens with npm (package-lock.json; it also deletes the hosted run's .npmrc), because the mode decision is ecosystem-agnostic.
Repro (Linux, Bun 1.4.3, main 4e146dd)
I used a local mock of the public patch proxy (SOCKET_PROXY_URL / SOCKET_PATCH_SERVER_URL / SOCKET_VENDOR_URL) that serves free patches for left-pad@1.3.0 (uuid 1111…) and minimist@1.2.2 (uuid 2222…).
mkdir mixed && cd mixed && git init -q
echo '{"name":"t","version":"1.0.0","dependencies":{"left-pad":"1.3.0","minimist":"1.2.2"}}' > package.json
echo node_modules/ > .gitignore
bun install && git add -A && git commit -qm init
# Path 1: partial takeover (#1039)
socket-patch scan --mode vendored --yes --json # both vendored
git add -A && git commit -qm vendored
# (the service answers left-pad's grant with pending_build)
socket-patch scan --mode hosted --yes --json # exit 0: left-pad kept vendored, minimist hosted,
# warnings: redirect_takeover_reverted_vendored
git add -A && git commit -qm "move to hosted"
socket-patch scan --yes --json # exit 0, status success, warnings []
git status --short
# M .socket/vendor/state.json
# M bun.lock ("minimist": ["minimist@.socket/vendor/npm/2222…/minimist-1.2.2.tgz", …])
# ?? .socket/vendor/npm/22222222-2222-4222-8222-222222222222/
# Path 2: per-package modes
socket-patch get left-pad --mode vendored --yes
socket-patch get minimist --mode hosted --yes
git add -A && git commit -qm mixed
socket-patch scan --yes --json # same: minimist converted hosted -> vendoredscan --dry-run --json predicts the conversion: {"action":"verified","purl":"pkg:npm/minimist@1.2.2","details":{"mode":"vendored"}}, also with no warning.
Expected vs actual
- Expected (CLI_CONTRACT.md "Mode resolution", docs/migrating-to-v5.md): a bare
scan/getnever converts a project. When a project holds vendored entries and hosted pins, the run should either keep each package in the mode it's already in, or refuse as ambiguous the way agent + vendored does (mode_ambiguous, exit 2, asking for--mode). Either way, it shouldn't run the hosted → vendored takeover. - Actual: exit 0,
status: success,warnings: [], and every hosted pin is converted to vendored. Human mode prints only the takeover note.
Matrix (Linux; "converts" = the bare run vendors the hosted pin, exit 0, no JSON warning)
| PM | Lock | How the mixed state was created | bare scan |
bare get minimist |
|---|---|---|---|---|
| Bun 1.4.3 | text bun.lock v2 |
partial --mode hosted takeover (#1039) |
converts | — |
| Bun 1.4.3 | bun.lockb |
partial --mode hosted takeover (#1039) |
converts | — |
| Bun 1.4.3 | text bun.lock v2 |
get --mode vendored + get --mode hosted |
converts | converts |
| Bun 1.4.3 | bun.lockb |
get --mode vendored + get --mode hosted |
converts | — |
| Bun 1.2.23 | text bun.lock v1 |
get --mode vendored + get --mode hosted |
converts (also in human mode, --dry-run predicts it) |
— |
| npm 10.9 | package-lock.json |
get --mode vendored + get --mode hosted |
converts (and deletes .npmrc) |
— |
| Bun 1.4.3 | workspace member, root vendored | — | refused redirect_workspace_lockfile_elsewhere, nothing written (fine) |
— |
After each conversion, a fresh clone's bun install --frozen-lockfile passes and is patched. The harm is the unrequested mode switch, not lost patch bytes. macOS and Windows weren't run; this is platform-independent mode-selection logic.
First bad commit: 3bcb267f (#1317) for this direction. On its parent 56ccbfc3, the same mixed project was converted the other way by a bare scan: left-pad was taken from vendored to hosted, the ledger and artifact were deleted, exit 0 (#1088 itself). So mixed hosted + vendored projects have never been left alone by a bare run. #1317 only flipped which half gets converted.
Suspect code
crates/socket-patch-cli/src/commands/mod.rs:81mode_from_project_state:vendored = ledger non-emptyandagent = manifest has uncovered records. The lockfiles' hosted pins never count as hosted state, and the doc comment says so ("neither → hosted (hosted mode keeps no ledger of its own)"). The(true, false)arm returnsVendoredeven when hosted pins are present.- The vendored flow then runs the hosted → vendored takeover for every hosted pin it meets (
Note: … was hosted; restored its upstream registry entry … (mode takeover)), with no check for whether the mode was explicit. - Possible direction: detect hosted pins (the same read-only discovery
rollback/listalready use), and treat vendored + hosted asmode_ambiguous. Or let the vendored flow skip the takeover leg when the mode wasn't explicit.
Related: #1088 / #1317 (the vendored → hosted direction), #1039 (partial takeover).
Activity
mikolalysenko commentedon Oct 12, 2026
[agent] Claiming this issue (root cause:
mode_from_project_statecounts only the vendor ledger and manifest, never hosted pins, so a mixed vendored + hosted project is classed vendored and the bare run takes over every hosted pin). Branch: agent/fix-bare-mode-mixed-hosted-vendored. Claim-ID: 2026-10-12T17:20:58Z-dccf60Generated by Claude Code
mikolalysenko commentedon Oct 12, 2026
[agent] Draft PR: #1440
Generated by Claude Code
Refuse bare scan/get on vendored + hosted projects
Count lockless hosted pins as hosted state
Count Gemfile-only gem pins as hosted state