Skip to content

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:

  1. !-negated patterns. The matcher applies npm-style negation, so ["packages/*", "!packages/b"] drops b. Yarn classic doesn't support negation in workspaces. Every release tested (1.0.2, 1.6.0, 1.10.1, 1.22.22) still treats packages/b as a workspace: it's resolved into the root yarn.lock, gets its own nested node_modules, and yarn workspaces info lists it.
  2. extglob patterns (packages/@(a|b), packages/+(a|b)). Yarn 1 globs workspaces with 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 uses glob, 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 unchanged

Control: 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 for packages/* 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 a yarn.lock (classic) root, ! patterns don't exclude anything, and extglobs match.
  • Actual: success / redirected: 0 / exit 0. The only warning is redirect_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


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

  1. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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"] and packages/a depending on left-pad@1.3.0:

    Bun packages/a in the root bun.lock workspaces map? hosted scan from packages/a hosted get <uuid> from packages/a
    1.1.39 (v0 text lock) yes (negation ignored) success, 0 scanned (hoisted; nothing installed in the member) exit 0, redirected: 0, redirect_npm_no_lockfile only
    1.2.0 yes — —
    1.2.23, linker = "isolated" yes exit 0, success, 1 scanned, redirected: 0, redirect_npm_no_lockfile (×2) exit 0, same (×2)
    1.2.23, hoisted (default) yes success, 0 scanned exit 0, redirected: 0
    1.3.0 / 1.3.4 / 1.3.9 / 1.4.2 no (negation honoured; a is a standalone lockless dir) n/a n/a (correct)

    On 1.2.23 the member is installed from the root lock. A fresh bun install --frozen-lockfile installs the upstream left-pad into 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 root bun.lock itself: its "workspaces" map lists exactly the member paths Bun resolved ("packages/a": {…}), on lockfileVersion 0/1/2. A binary bun.lockb would need the workspace-path table instead, or fail closed.

    The extglob half doesn't apply to Bun: packages/@(a|b) makes bun install fail with a package.json error on both 1.2.23 and 1.4.2.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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) and packages/!(z) as matching packages/a: it links node_modules/a → packages/a, puts the member's deps in the root package-lock.json, and nests left-pad@1.3.0 under packages/a/node_modules (the root depends on left-pad@1.2.0). A hosted run from packages/a then reports success without pinning anything:

    mkdir -p ws/packages/a && cd ws
    echo '{"name":"root","version":"1.0.0","private":true,"workspaces":["packages/@(a|b)"],"dependencies":{"left-pad":"1.2.0"}}' > package.json
    echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
    npm install            # node_modules/a is a link; packages/a/node_modules/left-pad is 1.3.0
    cd packages/a
    socket-patch scan --mode hosted --json --yes      # or: get <uuid> --mode hosted
    # -> exit 0, status success, redirect.redirected 0, warnings [redirect_npm_no_lockfile]
    #    root package-lock.json unchanged; human output: "Switched 0 packages to hosted patches"
    npm packages/* (control) packages/@(a|b) packages/+(a|c) packages/!(z)
    7.24.2 (Node 22) — exit 0, nothing pinned (scan + get) fail (scan + get) —
    8.19.4 (Node 22) — fail (scan + get) fail (scan + get) —
    10.9.4 (Node 22) refused redirect_workspace_lockfile_elsewhere (scan + get) fail (scan + get) fail (get) fail (get)
    12.2.0 (Node 24.21) — fail (scan + get) fail (scan + get) —

    In every cell npm installed packages/a as a workspace member. On 10.9.4 the #1071 shapes now refuse as they should (packages/{a,b}, packages/[a-c], and also packages/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

  3. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Same miss through get, on main ea09714 (yarn 1.22.22, ×2).

    Setup: root workspaces: ["packages/*", "!packages/b"] with left-pad@1.2.0. Member b depends on left-pad@1.3.0, so yarn installs it nested in packages/b/node_modules. From packages/b, get <uuid> --mode hosted --json exits 0 success, redirected: 0, with only redirect_npm_no_lockfile. Nothing is written to the root yarn.lock.

    On the ["packages/*"] control, the same get exits 1 with redirect_workspace_lockfile_elsewhere. So get shares the scan matcher gap, and a fix should cover both entry points.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions