Repository navigation
Hosted yarn berry rollback/remove put back a descriptor that yarn remove deleted from one workspace member, so every yarn install --immutable fails YN0028 #1435
Description
[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).
Summary
A hosted berry pin covers each descriptor of the patched package with its own resolutions selector (left-pad@npm:1.3.0, left-pad@npm:^1.3.0, …) and keys the lock entry by the tarball URL. If a user then runs yarn remove in one workspace member while another member still depends on the package, yarn keeps the URL-keyed entry (it's still live) and leaves both selectors in the root package.json. That state installs fine.
rollback and remove <pkg> then rebuild the restored entry's key from every selector that routes to the URL (restore_berry), including the one whose descriptor no workspace declares any more. Both exit 0, and every fresh yarn install --immutable fails:
➤ YN0028: │ -"left-pad@npm:1.3.0, left-pad@npm:^1.3.0":
➤ YN0028: │ +"left-pad@npm:^1.3.0":
➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
#1203's fix (#1321) retires a selector only when the URL entry is gone entirely (berry_selector_routes_nothing), so this partial case isn't covered.
Impact
An ordinary yarn remove in a monorepo, followed by the documented undo, leaves a repo where every CI yarn install --immutable fails, after the CLI reported success. The user has to run a mutable yarn install and commit the result.
Repro (Linux, yarn 4.18.1; a local mock patch API serving a real patched tarball)
mkdir -p proj/packages/a proj/packages/b && cd proj
echo '{"name":"proj","private":true,"workspaces":["packages/*"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"^1.3.0"}}' > packages/b/package.json
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
yarn install
socket-patch scan --mode hosted --yes ... # pins both selectors -> one URL-keyed entry
(cd packages/a && yarn remove left-pad) # b still uses left-pad; fresh --immutable still passes
socket-patch rollback ... # exit 0 (same with `remove left-pad`)
# fresh checkout:
yarn install --immutable # YN0028 (diff above)Control: the same steps without socket-patch pass yarn install --immutable.
Expected vs actual
- Expected: CLI_CONTRACT.md says rollback/remove put the project back on its upstream entries. The restored key should list only the descriptors the workspaces still declare (or the lock's live key), so that the next
yarn install --immutablepasses. Stale selectors should be retired as Retire berry resolutions left by yarn remove (#1203) #1321 already does for a fully removed package (hosted_resolution_orphaned). - Actual: exit 0, restored key
"left-pad@npm:1.3.0, left-pad@npm:^1.3.0", YN0028 on every immutable install, plain or hardened.
Matrix (main 1779317, Linux, node-modules linker)
| yarn | rollback |
remove left-pad |
scan --mode hosted --prune then remove |
|---|---|---|---|
| 4.0.2 | fail (exit 0, YN0028) | not run | not run |
| 4.18.1 | fail (exit 0, YN0028) | fail (exit 0, YN0028) | fail (redirect_prune_ignored; same YN0028) |
Release 4.0.0 is not affected. It pinned through ::__archiveUrl= in the npm-keyed lock entry, which yarn remove rewrites itself. So the bug arrived with the resolutions pin.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:805-842: for a URL-keyed entry the key is descriptors = selectors routed to the URL with no check against what the workspaces (or the lock's dependency maps) still declare. retire_stale_selectors (npm.rs:1062) only handles the case where the whole URL entry is gone.
No probe runs (Linux sandbox only).
Activity
mikolalysenko commentedon Oct 12, 2026
[agent] Triage: confirmed the suspect path on main
f8c7371. Inrestore_berry(crates/socket-patch-core/src/patch/redirect/upstream/npm.rs), a URL-keyed entry gets its restored key from everyresolutionsselector routed to the tarball URL. Nothing checks those selectors against the descriptors the workspaces still declare.retire_stale_selectorsonly covers a URL entry that is gone entirely (#1321). This is not a duplicate of #1203, which was the full-removal case. Labeledpriority:p2to match the other yarn-berry issues.Generated by Claude Code
mikolalysenko commentedon Oct 12, 2026
[agent] Claiming this issue (shared root cause: hosted berry restore rebuilds the lock key from every routed selector, including ones no workspace declares any more). Branch: agent/fix-berry-restore-stale-selectors. Claim-ID: 2026-10-12T16:20:58Z-1750ff
Generated by Claude Code
mikolalysenko commentedon Oct 12, 2026
[agent] Draft fix PR: #1436
Generated by Claude Code
Keep removed descriptors out of berry restore
Match declared berry descriptors exactly
Rebuild vendored berry revert from the live lock