Skip to content

Hosted and vendored scans refuse an independent Bun project in a dot-directory (packages/.tools) as a workspace member, though Bun's packages/* never lists dot-directories, so that project can't be patched at all #1631

Open

Description

[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).

Summary

socket-patch's Bun workspace-glob dialect lets wildcards match path components that start with . (WorkspacesDialect::BUN / BUN_PRE_1_3 have dot: true). Real Bun doesn't: with "workspaces": ["packages/*"] (or packages/**), Bun 1.1.39, 1.2.23, 1.3.9 and 1.4.3 never list packages/.tools as a member. A project there with its own bun.lock / bun.lockb is an independent project, and Bun installs it from that lock.

hosted::governing_root treats it as a member anyway:

  • scan --mode hosted / get --mode hosted from inside it (or with --cwd / a PATH argument) refuse with redirect_workspace_lockfile_elsewhere: "is a member of the Bun workspace rooted at … Bun installs it from …/bun.lock and ignores its own ./bun.lock". Exit 1, nothing written.
  • scan --mode vendored refuses with vendor_lockfile_missing (exit 1, partialFailure).
  • The suggested remedy (run from the root) doesn't patch it: the root crawl skips hidden directories (docs/ecosystems.md, "which node_modules trees are crawled"). The root run exits 0 success, patches only the root's packages, and gives no nested_project_skipped warning.

Impact

A hosted or vendored user has no way to patch a Bun project that lives in a dot-directory under a workspace glob (for example packages/.tools, packages/.storybook, or apps/.internal). The refusal fires when it shouldn't (it names a lock Bun never uses for that directory), and following its advice leaves the project unpatched without any warning. Only agent mode works there. Compare a !-excluded member ("workspaces": ["packages/*", "!packages/tools2"]), which Bun also treats as independent: the root run warns nested_project_skipped, and the member run pins its own lock (exit 0).

Repro (Linux; B = a real Bun binary; mock patch API as in the ledger method notes)

mkdir -p w/packages/a w/packages/.tools && cd w && git init -q
echo '{"name":"root","private":true,"workspaces":["packages/*"],"dependencies":{"is-number":"7.0.0"}}' > package.json
echo '{"name":"a","version":"1.0.0"}' > packages/a/package.json
echo '{"name":"tools","private":true,"dependencies":{"left-pad":"1.3.0"}}' > packages/.tools/package.json
bun install                               # root bun.lock lists only "packages/a"
(cd packages/.tools && bun install)       # .tools gets its own bun.lock + node_modules/left-pad
grep -o '"packages/[^"]*"' bun.lock       # -> "packages/a"  (Bun does not see .tools)
cd packages/.tools
socket-patch scan --mode hosted --json --yes    # exit 1, error.code redirect_workspace_lockfile_elsewhere
socket-patch scan --mode vendored --json --yes  # exit 1, vendor_lockfile_missing
cd ../.. && socket-patch scan --mode hosted --json --yes   # exit 0, only is-number pinned, no nested_project_skipped

Bun's own listing for other patterns, measured on 1.2.23 and 1.4.3: packages/* lists packages/a but not packages/.hidden, and packages/** lists packages/a and packages/x/y but not packages/.cache/x. Braces, character classes, ./packages/*, packages/*/ and ? all match the way socket-patch models them. Only the dot rule differs.

Expected vs actual

  • Expected (crates/socket-patch-cli/CLI_CONTRACT.md, redirect_workspace_lockfile_elsewhere): workspaces patterns are "read the way the root's lock's package manager reads them", and "only the root's locks whose package manager lists the directory govern it". Bun doesn't list packages/.tools, so the run should pin packages/.tools/bun.lock (as it does for a !-excluded member, or for the control tools/ outside the globs).
  • Actual: refused as a member, and the root run silently skips it.

OS × version

OS Bun Lock hosted (member run) vendored (member run)
Linux 1.1.39 text v0 refused (reproduces) not run
Linux 1.2.23 text v1 / bun.lockb refused / refused refused / refused
Linux 1.3.9 text v1 / bun.lockb refused / refused refused / refused
Linux 1.4.3 text v2 / bun.lockb refused / refused refused / refused
Linux 1.4.3 control: !packages/tools2 pass (exit 0, pins own lock) n/a
macOS / Windows any untested (no probe branch this run)

Tested on main 7d3086f (release 5.0.1). I didn't bisect it. The dot rule has been in place since the Bun dialects landed with #1480 / #1097.

Suspect code

  • crates/socket-patch-core/src/utils/workspace_globs.rs:89-104: WorkspacesDialect::BUN and BUN_PRE_1_3 set dot: true. Bun's workspace glob skips dot components for wildcards; an explicit .tools segment still matches.
  • crates/socket-patch-core/src/hosted/governing_root.rs:604 (lock_dialect → workspaces_include_as) is where the member verdict is made.

Activity

  1. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (Bun). Confirmed on main 7d3086f: WorkspacesDialect::BUN and BUN_PRE_1_3 (crates/socket-patch-core/src/utils/workspace_globs.rs) set dot: true, so packages/* matches packages/.tools in governing_root's member check, while the root crawl skips hidden dirs. This isn't a duplicate. It's separate from #1627 / PR #1628 (SKIP_DIRS names in the npm workspace walk). No open PR covers it.


    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