Configuring a Project .npmrc for Consistent Installs
Two developers can run the same package manager version on the same lockfile and still get different results, because install behaviour is shaped by configuration: registry URLs, peer-dependency handling, hoisting, script policy, engine checks and save-prefix rules. When that configuration lives in each developer's personal ~/.npmrc, it differs from machine to machine and from CI. A committed project .npmrc — plus pnpm-workspace.yaml for pnpm 10 settings — makes install behaviour part of the repository. This guide explains how .npmrc files are layered, which settings belong in the project, and how to keep credentials out of it.
Symptoms of configuration drift
Configuration drift produces failures that look like dependency bugs:
# Developer A (personal ~/.npmrc has legacy-peer-deps=true)
$ npm install # succeeds, lockfile resolved without strict peers
# CI (no such setting)
npm error code ERESOLVE
npm error ERESOLVE could not resolve
npm error While resolving: @acme/web@1.0.0
npm error Found: react@19.0.0
npm error Could not resolve dependency:
npm error peer react@"^18.0.0" from some-ui-kit@4.2.1
# Developer B (personal registry mirror in ~/.npmrc)
$ git diff package-lock.json | grep resolved | head -2
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
+ "resolved": "https://mirror.internal.example/zod/-/zod-3.24.1.tgz",
The first is a peer-resolution setting that only one machine has. The second writes a private mirror's URLs into the shared lockfile, which then fails for anyone outside that network. Both are fixed by moving the relevant settings into the project, where they are reviewed, versioned and identical for everyone who clones the repository.
How .npmrc files are layered
npm and pnpm read configuration from several places and merge them, with more specific sources winning:
The project file sits above the user file, so a setting in the repository overrides a conflicting personal preference — which is exactly what you want for behaviour-affecting options. Environment variables and flags still win, which lets CI override deliberately (for example, a mirror registry in a particular network).
pnpm 10 adds pnpm-workspace.yaml as the preferred place for pnpm-specific settings (such as onlyBuiltDependencies, overrides, catalog, nodeLinker), while registry and auth settings stay in .npmrc. Yarn Berry ignores .npmrc entirely and reads .yarnrc.yml.
A project .npmrc that pins behaviour
# .npmrc — committed. No secrets in this file.
# Registries: public by default, private scope routed explicitly
registry=https://registry.npmjs.org/
@acme:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
# Fail on wrong Node.js / package manager versions instead of warning
engine-strict=true
# Peer dependencies: be explicit about the policy everyone uses
auto-install-peers=true
strict-peer-dependencies=false
# How new dependencies are written to package.json
save-prefix=^
save-exact=false
# Supply-chain hardening (npm): no install scripts from dependencies
# ignore-scripts=true
# Reproducibility
fund=false
audit-level=high
And the pnpm-specific counterpart:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
onlyBuiltDependencies:
- esbuild
- "@swc/core"
publicHoistPattern:
- "*eslint*"
- "*prettier*"
What each group does:
- Registry routing — sends
@acme/*to the private registry and everything else to the public one, identically for every developer and CI job. Details in Routing Scopes to Multiple Registries in .npmrc. engine-strict— turnsenginesmismatches into errors, which also powers the package-manager guard described in Enforcing a Single Package Manager with only-allow.- Peer settings — the single biggest source of "resolves on my machine" differences; decide once and commit it.
- Save rules — stop pull requests mixing
^1.2.3and1.2.3depending on who added the dependency. - Script policy —
ignore-scriptsfor npm, or pnpm'sonlyBuiltDependenciesallowlist, applies to everyone rather than to the security-conscious few.
Settings that change the lockfile
Not every .npmrc option matters equally. The ones worth committing are those that change what gets resolved or written, because they are the ones that make two machines disagree about the lockfile. A useful test: if changing the setting and running an install changes package-lock.json or pnpm-lock.yaml, the setting belongs in the repository.
For npm, the resolution-affecting set is small: registry and scoped registries, legacy-peer-deps, strict-peer-deps, install-strategy (hoisted, nested, shallow or linked), omit defaults, and package-lock itself. For pnpm, it is larger: node-linker, hoist-pattern, public-hoist-pattern, auto-install-peers, dedupe-peer-dependents, resolution-mode, strict-peer-dependencies, overrides, patches and catalogs — pnpm records many of these in the lockfile and fails frozen installs when they change, as described in Fixing pnpm ERR_PNPM_OUTDATED_LOCKFILE in CI.
CI-specific overrides
CI sometimes needs different behaviour from developer machines, and the precedence rules make that easy without editing the committed file. A runner inside a corporate network may need an internal mirror: set npm_config_registry in the job environment, which overrides the project file for that job only. A security scanning job may want npm_config_ignore_scripts=true even if the project allows scripts. Because environment variables sit above the project file, these overrides are visible in the workflow definition and do not leak into developers' installs.
Be careful with mirrors in particular. A mirror that serves the same packages is harmless for installs, but if the lockfile is regenerated in that environment, its resolved URLs point at the mirror. Keep lockfile regeneration — dependency bots, manual updates — on the canonical registry, and use mirrors only for installs from an existing lockfile. pnpm avoids most of this by recording tarball URLs relative to the registry when they match the configured registry, but npm writes absolute URLs.
Keeping credentials out of the committed file
Never commit tokens. Reference them with environment variables, which npm and pnpm expand at read time:
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
Developers put the actual values in their shell environment or their personal ~/.npmrc (as a full line such as //npm.pkg.github.com/:_authToken=ghp_...), and CI sets the variables from its secret store. If a variable is unset, npm sends an empty token, which produces a clear 401 rather than silently falling back — see Fixing npm 401 Unauthorized on a Private Registry.
A pre-commit secret scanner or a simple CI grep for _authToken= lines that do not contain ${ catches accidental commits of literal tokens.
Worked example: removing "works on my machine" peer failures
A team's CI fails intermittently with ERESOLVE on pull requests from some developers but not others. Comparing npm config ls -l between machines shows that three developers had legacy-peer-deps=true in their personal ~/.npmrc, added years ago to get past a conflict, and their lockfiles were resolved under that setting. The team decides on the policy explicitly: fix the two real peer conflicts, commit legacy-peer-deps=false (the default, stated explicitly) in the project .npmrc, and regenerate the lockfile once. The project setting overrides the personal ones, so every machine now resolves identically, and the intermittent failures stop. The underlying conflicts are covered in Fixing npm ERESOLVE Peer Dependency Conflicts.
CLI validation and debug commands
# Every effective setting and where it came from
npm config ls -l | head -40
npm config get registry
npm config get @acme:registry
# Which config files are in play
npm config get userconfig
npm config get globalconfig
# pnpm's view, including workspace settings
pnpm config list
# Catch literal tokens in the committed file
grep -nE "_authToken=[^$]" .npmrc && echo "literal token committed!" || echo "no literal tokens"
Prevention and CI/CD guardrails
- Commit every behaviour-affecting setting in the project
.npmrcorpnpm-workspace.yaml. - Reference credentials only as
${VAR}, and scan for literal tokens in CI. - State defaults explicitly for settings that people commonly override personally, such as peer handling.
- Review
.npmrcchanges like code — a one-line registry change affects every install.
Frequently Asked Questions
Does npm read .npmrc files in workspace packages?
Only the project .npmrc at the workspace root (where commands run) applies to workspace installs. .npmrc files inside package folders are not merged for workspace-wide installs, which is a common source of confusion.
Is .npmrc used when my package is installed by consumers?
No. .npmrc is never read from a dependency's folder, and npm excludes it from published tarballs. It affects only commands run in your repository.
Should I commit ignore-scripts=true?
It is a strong supply-chain control, but it also disables your own prepare script during installs and blocks dependencies that genuinely need build scripts. With pnpm, the onlyBuiltDependencies allowlist is a more precise alternative.
Why does my project .npmrc seem to be ignored?
Check where you run the command. npm reads the project .npmrc from the directory containing the nearest package.json at the prefix it is operating on — the workspace root for workspace commands. Running from a subfolder that has its own package.json outside the workspace uses that folder's .npmrc instead. npm config ls -l shows which file each value came from.
Can I comment settings in .npmrc?
Yes. Lines starting with # or ; are comments, and they are worth using: a short note next to each non-default setting explaining why it exists stops someone deleting it during a cleanup.
Related
- Package Manager Version Management covers pinning the tool that reads this configuration.
- Routing Scopes to Multiple Registries in .npmrc goes deeper on registry configuration.
- Blocking Malicious Install Scripts with --ignore-scripts explains the script policy settings.
- Lockfile Management Strategies shows why consistent resolution settings matter for lockfiles.