install: fall back to $HOME/.npmrc when $XDG_CONFIG_HOME is set

Merged
RO
3mo ago
+89-23

Fixes #24124. Fixes #22971. Related: #23128 (the .npmrc half), supersedes the closed #26040.

Problem

  • With XDG_CONFIG_HOME exported (GitHub Actions ubuntu-latest sets it to ~/.config; many Linux desktops set it too), bun publish fails with error: missing authentication (run bunx npm login) even though ~/.npmrc holds a valid token and npm whoami succeeds. bun install likewise ignores a registry configured in ~/.npmrc.
  • Cause: PackageManager::init (src/install/PackageManager.rs, the initialize_store() block) picked the user-level .npmrc directory with XDG_CONFIG_HOME.get().or_else(|| HOME.get()), so once XDG_CONFIG_HOME was set, $HOME/.npmrc was never read, whether or not $XDG_CONFIG_HOME/.npmrc existed.
  • npm login writes $HOME/.npmrc; npm reads that file and ignores XDG_CONFIG_HOME.
  • Not covered here: actions/setup-node writes $RUNNER_TEMP/.npmrc and points NPM_CONFIG_USERCONFIG at it. bun does not read NPM_CONFIG_USERCONFIG, so that setup (#14824) still fails after this change.

Fix

  • Use $XDG_CONFIG_HOME/.npmrc when that file exists (keeps working for users who already put it there), otherwise $HOME/.npmrc. An empty XDG_CONFIG_HOME or HOME is treated as unset (get_not_empty()). The project-local .npmrc is still loaded afterwards and still takes precedence, as before.
  • Tests: test/cli/install/npmrc.test.ts, describe("user .npmrc lookup"), four cases driven through bun publish --dry-run (needs the token, never contacts a registry; the Registry: line in its output shows which file was read):
    • $XDG_CONFIG_HOME/.npmrc present: it wins
    • XDG_CONFIG_HOME set, no file there: $HOME/.npmrc is used (fails on main)
    • XDG_CONFIG_HOME unset: $HOME/.npmrc is used
    • XDG_CONFIG_HOME="": $HOME/.npmrc is used (fails on main)
  • USE_SYSTEM_BUN=1 bun test test/cli/install/npmrc.test.ts -t "user .npmrc lookup": 2 pass, 2 fail (missing authentication)
  • bun bd test test/cli/install/npmrc.test.ts: 31 pass (whole file)

Background

  • .npmrc is npm's ini-style config file (registry URL, //host/:_authToken= credentials, and so on). Bun reads up to two of them on every install-family command: one user-level file and the project's ./.npmrc, later files overriding earlier ones (ini::load_npmrc_config).
  • XDG_CONFIG_HOME is the freedesktop variable for a user's config directory, usually ~/.config. Bun has historically also looked for .npmrc directly inside it; npm does not.

[review] gate passed · iteration 6 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/npmrc.test.ts
bun test v1.4.0 (930b7af16)

test/cli/install/npmrc.test.ts:
{
  out: "/tmp/verdaccio-test-_Fyh66Z/hi!",
  err: "",
}
(pass) npmrc > should convert to utf8 if BOM [348.58ms]
package dir /tmp/verdaccio-test-_IeyYIw
bun install v1.4.0 (930b7af16)
No packages! Deleted empty lockfile

[108.00ms] done
(pass) npmrc > works with empty file [281.54ms]
package dir /tmp/verdaccio-test-_9ODdIL
bun install v1.4.0 (930b7af16)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [411.00ms]
(pass) npmrc > sets default registry [536.32ms]
bun install v1.4.0 (930b7af16)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ @types/no-deps@1.0.0 (v2.0.0 available)

1 package installed [570.00ms]
(pass) npmrc > sets scoped registry [727.22ms]
package dir /tmp/verdaccio-test-_Y7OsCk
home dir /tmp/verdaccio-test-_Y7OsCk/home_dir
bun install v1.4.0 (930b7af16)
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package i
... (truncated)

release without fix: 2 FAILED
bun test v1.4.0-canary.1 (da3851e57)

test/cli/install/npmrc.test.ts:
{
  out: "/tmp/verdaccio-test-_L3vvhh/hi!",
  err: "",
}
(pass) npmrc > should convert to utf8 if BOM [6.60ms]
package dir /tmp/verdaccio-test-_WPggNn
bun install v1.4.0-canary.1 (da3851e57)
No packages! Deleted empty lockfile

[1.00ms] done
(pass) npmrc > works with empty file [8.16ms]
package dir /tmp/verdaccio-test-_I9szLQ
bun install v1.4.0-canary.1 (da3851e57)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [28.00ms]
(pass) npmrc > sets default registry [48.01ms]
bun install v1.4.0-canary.1 (da3851e57)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ @types/no-deps@1.0.0 (v2.0.0 available)

1 package installed [314.00ms]
(pass) npmrc > sets scoped registry [333.60ms]
package dir /tmp/verdaccio-test-_ck6dGm
home dir /tmp/verdaccio-test-_ck6dGm/home_dir
bun install v1.4.0-canary.1 (da3851e57)
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [169.00ms]
(pass) npmrc > works with home config [296.00ms]
package dir /tmp/verdaccio-test-_5UnG2b
home dir /tmp/verd
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/npmrc.test.ts
bun test v1.4.0 (930b7af16)

test/cli/install/npmrc.test.ts:
{
  out: "/tmp/verdaccio-test-_SV2m9B/hi!",
  err: "",
}
(pass) npmrc > should convert to utf8 if BOM [342.55ms]
package dir /tmp/verdaccio-test-_YWNf8S
bun install v1.4.0 (930b7af16)
No packages! Deleted empty lockfile

[1184.00ms] done
(pass) npmrc > works with empty file [1519.19ms]
package dir /tmp/verdaccio-test-_CGgs8v
bun install v1.4.0 (930b7af16)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [1092.00ms]
(pass) npmrc > sets default registry [1273.03ms]
bun install v1.4.0 (930b7af16)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ @types/no-deps@1.0.0 (v2.0.0 available)

1 package installed [418.00ms]
(pass) npmrc > sets scoped registry [586.55ms]
package dir /tmp/verdaccio-test-_ZePx90
home dir /tmp/verdaccio-test-_ZePx90/home_dir
bun install v1.4.0 (930b7af16)
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 packa
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1126ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/252] gen generated_host_exports.rs
generated_host_exports.rs: 93 exports (host=3, lazy=10, generic=80, rust=0); 239 extern-C blocks audited
[1/252] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

^[[1m^[[92m   Compiling^[[0m bun_io v0.0.0 (/workspace/bun/src/io)
^[[1m^[[92m   Compiling^[[0m bun_parsers v0.0.0 (/workspace/bun/src/parsers)
^[[1m^[[92m   Compiling^[[0m bun_react_compiler v0.0.0 (/workspace/bun/src/react_compiler)
^[[1m^[[92m   Compiling^[[0m bun_zlib v0.0.0 (/workspace/bun/src/zlib)
^[[1m^[[92m   Compiling^[[0m bun_event_loop v0.0.0 (/workspace/bun/src/event_loop)
^[[1m^[[92m   Compiling^[[0m bun_sourcemap v0.0.0 (/workspace/bun/src/sourcemap)
^[[1m^[[92m   Compiling^[[0m bun_css v0.0.0 (/workspace/bun/src/css)
^[[1m^[[92m   Compiling^[[0m bun_options_types v0.0.0 (/workspace/bun/src/options_types)
^[[1m^[[92m   Compiling^[[0m bun_http v0.0.0 (/workspace/bun/src/http)
^[[1m^[[92m   Compiling^[[0m
... (truncated)
diff hotspot
src/install/PackageManager.rs  | 56 +++++++++++++++++++++++++-----------------
 test/cli/install/npmrc.test.ts | 56 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+), 23 deletions(-)

gate history · 2 passed · 2 rejected · iteration 6

evidence per changed file
file                            reads  edits  tests
src/install/PackageManager.rs       4      3      0
test/cli/install/npmrc.test.ts      2      5      0