Summary
cafile=<relative-path> in .npmrc is read via loadCAFile:
function loadCAFile (cafilePath: string): string[] | undefined {
let contents: string | undefined
try {
contents = fs.readFileSync(cafilePath, 'utf8')
} catch { /* ... */ }
// ...
}
fs.readFileSync with a relative path resolves against process.cwd(). pnpm's CLI doesn't process.chdir(opts.dir) when --dir is passed, so a project .npmrc containing a relative cafile= is read from the wrong directory when the user runs pnpm from somewhere other than the project root.
Reproducer
mkdir -p /tmp/project
echo "cafile=certs/ca.pem" > /tmp/project/.npmrc
mkdir /tmp/project/certs
# (any valid PEM)
cp /etc/ssl/cert.pem /tmp/project/certs/ca.pem
cd /home/user # NOT /tmp/project
pnpm --dir /tmp/project install
Expected: pnpm loads /tmp/project/certs/ca.pem (resolved against the directory of the .npmrc that contains the cafile= key).
Actual: pnpm reads /home/user/certs/ca.pem, fails open via the try { ... } catch {} swallow, and the install proceeds without the custom CA. The user gets either a successful install (when the system CA bundle happens to cover the registry) or a TLS error that doesn't mention the missing cafile.
Why it bites
- The user-facing intent of "put a
cafile in the project's .npmrc" is "this CA applies to this project". Resolving the path relative to whatever cwd happens to be active when pnpm runs breaks that intent the moment --dir is used (CI scripts, monorepo wrappers, npx-style invocations, etc.).
- The failure is silent. The
try/catch in loadCAFile drops the CA list to undefined, the install continues, and the user only notices when TLS errors surface against a private registry that needed the missing CA. There's no warning, no telemetry, no log line tying the error back to the misresolved path.
- Other path-valued
.npmrc keys (notably cert=, key=) have the same shape and likely the same bug — loadNpmrcFiles.ts only does path.resolve for ${VAR}-substituted values, never for plain relative paths.
Proposed fix
loadCAFile (and the analogous read sites for cert= / key=) should resolve relative paths against the directory the .npmrc was read from, not process.cwd(). Concretely:
// inside loadNpmrcFiles, where the .npmrc dir is already known:
const cafilePath = path.isAbsolute(rawCafilePath)
? rawCafilePath
: path.resolve(npmrcDir, rawCafilePath)
npmrcDir is already available at the call site — loadNpmrcFiles walks the cwd / home / global hierarchy and knows which directory each .npmrc came from.
The fix is backwards-compatible for absolute paths (the dominant production case — most CI templates use absolute paths). It changes behavior only for the relative-path case that's broken under --dir today.
Compatibility considerations
- Users currently working around the bug by passing an absolute path see no change.
- Users who happen to
cd into the project before running pnpm see no change (cwd === npmrcDir, so the resolution is identical).
- Users running
pnpm --dir /other install from elsewhere with a relative cafile= see a correctly resolved path where they currently see a silent failure.
The only behavior change is "the install now finds the CA file the user obviously wanted" — strictly an improvement.
Related downstream context
pacquet (a Rust port of pnpm) matches this behavior bug-for-bug per its own cardinal rule of "do not 'fix' pnpm quirks unless the same fix has landed upstream." Once a fix lands here, pacquet will port it. Reference: pnpm/pacquet#498.
Written by an agent (Claude Code, claude-opus-4-7).
Summary
cafile=<relative-path>in.npmrcis read vialoadCAFile:fs.readFileSyncwith a relative path resolves againstprocess.cwd(). pnpm's CLI doesn'tprocess.chdir(opts.dir)when--diris passed, so a project.npmrccontaining a relativecafile=is read from the wrong directory when the user runs pnpm from somewhere other than the project root.Reproducer
Expected: pnpm loads
/tmp/project/certs/ca.pem(resolved against the directory of the.npmrcthat contains thecafile=key).Actual: pnpm reads
/home/user/certs/ca.pem, fails open via thetry { ... } catch {}swallow, and the install proceeds without the custom CA. The user gets either a successful install (when the system CA bundle happens to cover the registry) or a TLS error that doesn't mention the missingcafile.Why it bites
cafilein the project's.npmrc" is "this CA applies to this project". Resolving the path relative to whatever cwd happens to be active when pnpm runs breaks that intent the moment--diris used (CI scripts, monorepo wrappers,npx-style invocations, etc.).try/catchinloadCAFiledrops the CA list toundefined, the install continues, and the user only notices when TLS errors surface against a private registry that needed the missing CA. There's no warning, no telemetry, no log line tying the error back to the misresolved path..npmrckeys (notablycert=,key=) have the same shape and likely the same bug —loadNpmrcFiles.tsonly doespath.resolvefor${VAR}-substituted values, never for plain relative paths.Proposed fix
loadCAFile(and the analogous read sites forcert=/key=) should resolve relative paths against the directory the.npmrcwas read from, notprocess.cwd(). Concretely:npmrcDiris already available at the call site —loadNpmrcFileswalks the cwd / home / global hierarchy and knows which directory each.npmrccame from.The fix is backwards-compatible for absolute paths (the dominant production case — most CI templates use absolute paths). It changes behavior only for the relative-path case that's broken under
--dirtoday.Compatibility considerations
cdinto the project before running pnpm see no change (cwd === npmrcDir, so the resolution is identical).pnpm --dir /other installfrom elsewhere with a relativecafile=see a correctly resolved path where they currently see a silent failure.The only behavior change is "the install now finds the CA file the user obviously wanted" — strictly an improvement.
Related downstream context
pacquet (a Rust port of pnpm) matches this behavior bug-for-bug per its own cardinal rule of "do not 'fix' pnpm quirks unless the same fix has landed upstream." Once a fix lands here, pacquet will port it. Reference:
pnpm/pacquet#498.Written by an agent (Claude Code, claude-opus-4-7).