Back to core workflows Fix dependency resolution Tune package metadata Validate before publishing

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:

.npmrc precedence, highest first Command-line flags override environment variables, which override the project .npmrc, then the user ~/.npmrc, then the global npmrc, then built-in defaults. 1 CLI flags --legacy-peer-deps, --registry — per invocation 2 npm_config_* env vars npm_config_registry, set by CI or shell 3 project .npmrc committed next to package.json — shared by everyone 4 user ~/.npmrc personal tokens and preferences 5 global npmrc installation-wide, rarely used
The project file is the highest-precedence layer you can commit, so shared behaviour belongs there.

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 — turns engines mismatches 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.3 and 1.2.3 depending on who added the dependency.
  • Script policyignore-scripts for npm, or pnpm's onlyBuiltDependencies allowlist, 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.

Does this setting belong in the project file? Classifies a configuration option by whether it changes resolution, contains a secret, or is a personal preference. A configuration option what does it affect? Commit it peers, overrides, hoisting, registry resolution / lockfile Environment only reference as ${VAR} credentials Keep it personal color, loglevel, progress output / UX
Resolution-affecting settings go in the repository; secrets and preferences never do.

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.

Where each kind of setting belongs Classifies common settings by whether they belong in the committed project .npmrc, pnpm-workspace.yaml, the user ~/.npmrc, or CI environment variables. project .npmrc pnpm-workspace.yam l user ~/.npmrc or env registry and scope routing yes no mirror overrides only auth tokens as ${VAR} only no actual values peer and engine policy yes pnpm-specific peers no build-script allowlist ignore-scripts (npm) onlyBuiltDependencies no editor, color, loglevel no no yes
Behaviour goes in the repository; secrets and personal preferences stay outside it.

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 .npmrc or pnpm-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 .npmrc changes 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