pnpm Hooks (.pnpmfile.mjs)
pnpm hooks customize installation. Declare them in .pnpmfile.mjs (ESM, preferred) or .pnpmfile.cjs (CommonJS), located next to the lockfile (workspace root for a monorepo).
The modern format uses ESM
export const hooks = { ... }. The old CommonJSmodule.exports = { hooks }still works in.pnpmfile.cjs.
Setup
export const hooks = {
readPackage,
afterAllResolved,
updateConfig,
beforePacking,
}Hook reference
| Hook | When | Use |
|---|---|---|
readPackage(pkg, ctx) |
after a dependency manifest is parsed | mutate a dependency's package.json (affects resolution) |
afterAllResolved(lockfile, ctx) |
after resolution | mutate the lockfile before it's written |
updateConfig(config) |
before install | mutate pnpm's settings (great with config dependencies) |
beforePacking(pkg) |
before pnpm pack/publish tarball |
customize the published manifest only |
preResolution(opts) |
after reading lockfiles, before resolution | inspect/modify lockfile objects |
importPackage(dir, opts) |
when writing to node_modules | deprecated (v11.23.0) — opts out of the parallel importer; will be removed |
filterLog(log) |
per log entry | deprecated/ignored (v12.0.0) — use loglevel instead |
readPackage
Called for every package before resolution. Common uses:
function readPackage(pkg, context) {
// Add a missing peer dependency
if (pkg.name === 'some-broken-package') {
pkg.peerDependencies = { ...pkg.peerDependencies, react: '*' }
}
// Pin a transitive version
if (pkg.dependencies?.lodash) pkg.dependencies.lodash = '^4.17.21'
// Drop a problematic optional dep
delete pkg.optionalDependencies?.fsevents
// Replace a deprecated dep
if (pkg.dependencies?.['old-pkg']) {
pkg.dependencies['new-pkg'] = pkg.dependencies['old-pkg']
delete pkg.dependencies['old-pkg']
}
return pkg
}
export const hooks = { readPackage }Mutations are not written to disk; they only affect resolution. Delete
pnpm-lock.yamlto re-resolve an already-locked dependency. Removingscriptshere does not stop a build — use theallowBuildssetting instead. To persist a change to a dependency's files, usepnpm patch.
updateConfig
Modify pnpm's own settings programmatically — most powerful when shipped in a config dependency so settings are shared across repos.
export const hooks = {
updateConfig(config) {
return Object.assign(config, {
enablePrePostScripts: false,
optimisticRepeatInstall: true,
resolutionMode: 'lowest-direct',
verifyDepsBeforeRun: 'install',
})
}
}// Add a catalog entry from a plugin
export const hooks = {
updateConfig(config) {
config.catalogs.default ??= {}
config.catalogs.default['is-odd'] = '1.0.0'
return config
}
}Since v12.4.1,
configis the resolved configuration (every setting pnpm will act on, from.npmrc, CLI, and defaults; unset keys are absent, notnull). It also carriesregistriesByScope(scope → registry URL; rewrite to redirect fetches) andconfigByUri(registry URI → credentials). Since v12.3.0 the pnpmfile is loaded by many more commands (run,exec,rebuild, script shortcuts,link,outdated,import,pack,publish,stage publish), soupdateConfigsettings likeextraEnv/extraBinPathsreach spawned processes and hook-provided catalogs resolve at pack time.
beforePacking
Customize the manifest that ends up in the published tarball without touching your local package.json.
export const hooks = {
beforePacking(pkg) {
delete pkg.devDependencies
pkg.main = './dist/index.js'
return pkg
}
}afterAllResolved
export const hooks = {
afterAllResolved(lockfile, context) {
context.log(`Resolved ${Object.keys(lockfile.packages || {}).length} packages`)
return lockfile
}
}Finders (pnpm list / why)
Custom predicates used via --find-by:
export const finders = {
react17: (ctx) => ctx.readManifest().peerDependencies?.react === '^17.0.0'
}pnpm why --find-by=react17Custom resolvers & fetchers (advanced)
Register top-level resolvers/fetchers to support new package schemes (e.g. my-protocol:pkg). Each is an object with cheap canResolve/canFetch guards plus resolve/fetch. Custom resolvers run before built-ins; custom resolution type fields must use the custom: prefix.
const resolver = {
canResolve: (dep) => dep.alias.startsWith('@company/'),
resolve: async (dep) => ({
id: `${dep.alias}@${dep.bareSpecifier}`,
resolution: { type: 'custom:cdn', cdnUrl: '...' },
}),
}
const fetcher = {
canFetch: (id, res) => res.type === 'custom:cdn',
fetch: (cafs, res, opts, fetchers) =>
fetchers.remoteTarball(cafs, { tarball: res.cdnUrl, integrity: res.integrity }, opts),
}
module.exports = { resolvers: [resolver], fetchers: [fetcher] }
hooks.fetcherswas removed in v11 — use the top-levelfetchersexport instead.
Delegating to built-in fetchers
Instead of fetching itself, a custom fetcher can return a { delegate } envelope naming a complete, fetchable resolution for pnpm to fetch with its built-in path (single-step; a custom-typed delegate is rejected):
fetch: (cafs, resolution) => ({
delegate: { tarball: resolution.customUrl, integrity: resolution.integrity },
})Prefer the envelope over calling
fetchers.*directly: it is the only form that works in both pnpm and pacquet (the Rust port), wherecafs/fetchersarrive asnullover IPC.
Related settings
ignorePnpmfile: false # ignore the pnpmfile entirely
pnpmfile: ['.pnpmfile.mjs'] # local pnpmfile location(s)
globalPnpmfile: ~/.pnpm/global_pnpmfile.mjsHooks vs Overrides
| Hooks (.pnpmfile) | Overrides (pnpm-workspace.yaml) | |
|---|---|---|
| Logic | JavaScript | declarative |
| Scope | any manifest field, config, lockfile, packing | versions |
| Use when | conditional/complex fixes | simple version pins |
Prefer overrides/packageExtensions for simple cases; use hooks for conditional logic, config sharing, or packing tweaks.
Key Points
- Prefer
.pnpmfile.mjswithexport const hooks/finders/resolvers/fetchers. - New hooks:
updateConfig(mutate settings),beforePacking(published manifest),preResolution,importPackage. - Pair
updateConfigwith config dependencies to share settings/catalogs across repos. --ignore-scriptsdoes not disable the pnpmfile; useignorePnpmfile.