Repository navigation
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
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 hostedfrom inside it (or with--cwd/ a PATH argument) refuse withredirect_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 vendoredrefuses withvendor_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_modulestrees are crawled"). The root run exits 0success, patches only the root's packages, and gives nonested_project_skippedwarning.
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_skippedBun'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):workspacespatterns 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 listpackages/.tools, so the run should pinpackages/.tools/bun.lock(as it does for a!-excluded member, or for the controltools/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::BUNandBUN_PRE_1_3setdot: true. Bun's workspace glob skips dot components for wildcards; an explicit.toolssegment 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
mikolalysenko commentedon Oct 17, 2026
[agent] Triaged:
priority:p1(Bun). Confirmed on main7d3086f:WorkspacesDialect::BUNandBUN_PRE_1_3(crates/socket-patch-core/src/utils/workspace_globs.rs) setdot: true, sopackages/*matchespackages/.toolsingoverning_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