Repository navigation
Hosted scan from a yarn classic workspace member still pins nothing and exits 0 when the root's workspaces use a ! pattern (yarn 1 ignores it) or an extglob like packages/@(a|b) #1097
Description
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
#884 / #901 and #1071 / #1073 made a hosted scan / get run from a workspace member refuse with redirect_workspace_lockfile_elsewhere, because the member installs from the root's yarn.lock, which the run can't see. The member matcher (workspaces_include in crates/socket-patch-core/src/hosted/governing_root.rs) still disagrees with yarn classic about who is a member in two cases. In both, the refusal doesn't fire, and the run reports success, redirected: 0, exit 0, while yarn keeps installing the member's unpatched copy from the root lock:
!-negated patterns. The matcher applies npm-style negation, so["packages/*", "!packages/b"]dropsb. Yarn classic doesn't support negation inworkspaces. Every release tested (1.0.2, 1.6.0, 1.10.1, 1.22.22) still treatspackages/bas a workspace: it's resolved into the rootyarn.lock, gets its own nestednode_modules, andyarn workspaces infolists it.- extglob patterns (
packages/@(a|b),packages/+(a|b)). Yarn 1 globsworkspaceswith node-glob / minimatch, which supports extglobs, so both members are workspaces. The matcher learnt braces and character classes in Fix workspace-member refusal for vlt and brace/class globs (#1071, #942) #1073 but not extglobs, so it matches nothing. (npm's map-workspaces also usesglob, so the extglob half probably applies to npm roots too. I tested only yarn.)
Impact
This is the #884 failure again: a CI step or developer running socket-patch scan --mode hosted from a member directory sees success with nothing pinned, and the next install brings in the unpatched package. Running from the root works (see below). Vendored mode fails closed from the member (vendor_lockfile_missing, exit 1), so only hosted is affected.
Repro (yarn 1.22.22, Linux)
mkdir -p ws/packages/{a,b} && cd ws
echo '{"name":"m-a","version":"1.0.0"}' > packages/a/package.json
echo '{"name":"m-b","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/b/package.json
# root pins another version so b keeps its own nested copy
echo '{"name":"root","version":"1.0.0","private":true,"workspaces":["packages/*","!packages/b"],"dependencies":{"left-pad":"1.2.0"}}' > package.json
# (extglob variant: "workspaces":["packages/@(a|b)"])
yarn install
yarn workspaces info # lists m-a AND m-b
grep '^left-pad@1.3.0' yarn.lock # b's dep is in the root lock
ls packages/b/node_modules # left-pad (1.3.0)
cd packages/b
socket-patch scan --mode hosted --json --yes # patch available for left-pad@1.3.0
# -> exit 0, status "success", redirect.redirected 0,
# warnings: [redirect_npm_no_lockfile]; root yarn.lock unchangedControl: with "workspaces": ["packages/*"], the same run from packages/b exits 1 with redirect_workspace_lockfile_elsewhere, and nothing is written. From the root, the negation project pins left-pad@1.3.0 (redirected: 1), and a frozen install puts the patched bytes in packages/b/node_modules/left-pad.
I ran this against a local mock patch API (--api-url, SOCKET_PATCH_SERVER_URL).
Expected vs actual
- Expected: the run refuses with
redirect_workspace_lockfile_elsewhere, as it does forpackages/*and, since Fix workspace-member refusal for vlt and brace/class globs (#1071, #942) #1073, for brace and class globs. CLI_CONTRACT.md says "A workspace member that shares its root's lockfile is part of that root's project". The member set should be the one the package manager that owns the root lock computes: for ayarn.lock(classic) root,!patterns don't exclude anything, and extglobs match. - Actual:
success/redirected: 0/ exit 0. The only warning isredirect_npm_no_lockfile, and nothing tells the user that the root lock still installs the unpatched copy.
Matrix (Linux, main e61a845, each cell run at least twice)
| yarn | packages/* (control) |
packages/* + !packages/b |
packages/@(a|b) |
packages/+(a|b) |
|---|---|---|---|---|
| 1.0.2 | refused (pass) | exit 0, nothing pinned | exit 0, nothing pinned | exit 0, nothing pinned |
| 1.6.0 | refused (pass) | fail | fail | fail |
| 1.10.1 | refused (pass) | fail | fail | fail |
| 1.22.22 | refused (pass) | fail | fail | fail |
In every cell, yarn installed packages/b as a workspace member: its left-pad@1.3.0 was in the root yarn.lock and nested under packages/b/node_modules. macOS / Windows: untested. The matcher is path-string logic, so it should behave the same there.
First bad: not a regression. The negation path dates from the #884 fix (#901); extglobs were never matched.
Suspect code
crates/socket-patch-core/src/hosted/governing_root.rs:486:workspaces_includetreats a leading!as an exclusion for every lock type. For a root whose lock is a classicyarn.lock, it shouldn't. Which lock governs is already known inpackage_json_workspace_refusal(WORKSPACE_ROOT_LOCKS), so it could be passed down.governing_root.rs:651segment_glob_matches: no@(…),+(…),*(…),?(…)or!(…)extglob support. A fail-closed option is to treat any pattern containing an extglob as matching, and refuse.- Related: Hosted scan/get run from a yarn classic workspace member still reports success while pinning nothing: the #598 governing-root refusal covers pnpm and cargo only #884, Hosted scan/get from an npm workspace member still pins nothing and exits 0 when the root's workspaces glob uses braces or a character class (packages/{a,b}, packages/[a-c]), because the #884 refusal's matcher doesn't support them #1071 (closed by Fix workspace-member refusal for vlt and brace/class globs (#1071, #942) #1073).
Backlog review — 2026-10-08
Priority: P1 → P2. Yarn workspace glob edge cases yield no hosted action. Keep the discovery fix, with a lower priority than a false safety attestation.
Activity
mikolalysenko commentedon Oct 8, 2026
[agent] Bun data from the Bun bug-hunt routine (ledger #306), Linux, main
fe8455d.The
!half reproduces on Bun < 1.3.0, and whether it applies depends on the Bun version, not on the lock type. With"workspaces": ["packages/*", "!packages/a"]andpackages/adepending onleft-pad@1.3.0:packages/ain the rootbun.lockworkspacesmap?scanfrompackages/aget <uuid>frompackages/aredirected: 0,redirect_npm_no_lockfileonlylinker = "isolated"success, 1 scanned,redirected: 0,redirect_npm_no_lockfile(×2)redirected: 0ais a standalone lockless dir)On 1.2.23 the member is installed from the root lock. A fresh
bun install --frozen-lockfileinstalls the upstreamleft-padinto the member, and a hosted scan from the root pins it (redirected: 1, frozen install patched). So the member run is the #884 false success. Vendored from the isolated member fails closed (exit 1), as with yarn.So for Bun the fix can't key on the lock type alone (
bun.lock→ ignore!), because Bun 1.3.0+ does exclude the member. One option that matches Bun on every version is to read membership from the rootbun.lockitself: its"workspaces"map lists exactly the member paths Bun resolved ("packages/a": {…}), on lockfileVersion 0/1/2. A binarybun.lockbwould need the workspace-path table instead, or fail closed.The extglob half doesn't apply to Bun:
packages/@(a|b)makesbun installfail with a package.json error on both 1.2.23 and 1.4.2.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] npm data from the npm bug-hunt routine (ledger #302), Linux, main
ea09714.The extglob half applies to npm roots too, on every npm version I tried. npm's map-workspaces treats
packages/@(a|b),packages/+(a|c)andpackages/!(z)as matchingpackages/a: it linksnode_modules/a → packages/a, puts the member's deps in the rootpackage-lock.json, and nestsleft-pad@1.3.0underpackages/a/node_modules(the root depends onleft-pad@1.2.0). A hosted run frompackages/athen reports success without pinning anything:packages/*(control)packages/@(a|b)packages/+(a|c)packages/!(z)redirect_workspace_lockfile_elsewhere(scan + get)In every cell npm installed
packages/aas a workspace member. On 10.9.4 the #1071 shapes now refuse as they should (packages/{a,b},packages/[a-c], and alsopackages/a/,./packages/a,packages/**), so #1073 is verified for npm. Vendored from the extglob member fails closed (exit 1), as with yarn. I ran this against a local mock patch API.For npm the fail-closed option in the issue body works as is, since npm honours
!negation and extglobs alike.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Same miss through
get, on mainea09714(yarn 1.22.22, ×2).Setup: root
workspaces: ["packages/*", "!packages/b"]withleft-pad@1.2.0. Memberbdepends onleft-pad@1.3.0, so yarn installs it nested inpackages/b/node_modules. Frompackages/b,get <uuid> --mode hosted --jsonexits 0success,redirected: 0, with onlyredirect_npm_no_lockfile. Nothing is written to the root yarn.lock.On the
["packages/*"]control, the samegetexits 1 withredirect_workspace_lockfile_elsewhere. Sogetshares thescanmatcher gap, and a fix should cover both entry points.Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Claiming this issue (shared root cause: the package.json workspace member matcher applies npm's
!negation and lacks extglobs, so its member set disagrees with yarn classic and npm). Branch: agent/fix-workspace-glob-dialects. Claim-ID: 2026-10-13T12:21:16Z-f63b15Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Draft PR: #1480
Generated by Claude Code
Read workspaces globs the way each manager does
mikolalysenko commentedon Oct 13, 2026
[agent] #1480 is ready for review. CI is green on
bfa67a3and Bugbot found no issues. It readsworkspacesthe way the root lock's package manager does. yarn classic and Bun < 1.3 ignore!. npm and Bun 1.3+ use last-match-wins, and yarn berry applies!in any order. npm and yarn now match extglobs. Bothscanandgetfrom such a member refuse withredirect_workspace_lockfile_elsewhere. This covers the npm-extglob and Bun negation comments too.Generated by Claude Code