/flags/pnpm/pnpm-install-shamefully-hoist
pnpm install --shamefully-hoist: a flat node_modules for tools that need it
- tool
- pnpm
- command
- pnpm install
- flag
- --shamefully-hoist
- default
- false (only direct dependencies are at the top of node_modules)
By default pnpm puts only your direct dependencies at the top of node_modules; their own dependencies live in node_modules/.pnpm and are reachable only by the packages that declared them. pnpm install --shamefully-hoist (setting shamefully-hoist=true) also links every transitive dependency into the root of node_modules, producing a flat layout like npm's. It is a compatibility switch for tools and old packages that import modules they never listed in package.json.
What changes
is-odd depends on is-number, but the project only lists is-odd:
cat > package.json <<'EOF'
{ "name": "demo", "version": "1.0.0", "dependencies": { "is-odd": "3.0.1" } }
EOF
npx --yes pnpm@9 install >/dev/null 2>&1
echo "--- default"
ls node_modules
node -e 'require("is-number")' 2>&1 | grep -m1 "Cannot find module"
rm -rf node_modules
npx --yes pnpm@9 install --shamefully-hoist >/dev/null 2>&1
echo "--- --shamefully-hoist"
ls node_modules
node -e 'console.log(typeof require("is-number"))'Output
--- default is-odd Error: Cannot find module 'is-number' --- --shamefully-hoist is-number is-odd function
Without hoisting, requiring is-number from the project fails with Cannot find module, because the project never declared it. With --shamefully-hoist the require succeeds; the dependency is still undeclared, it is just reachable.
Hoisting settings compared
| Setting | Result |
|---|---|
| (default) | Direct dependencies at the top; everything else only in .pnpm |
| public-hoist-pattern | Hoist only matching packages to the root (default includes eslint and prettier plugins in older releases) |
| hoist-pattern | Hoist into the hidden .pnpm/node_modules, visible to packages but not to your code |
| shamefully-hoist=true | Same as public-hoist-pattern=*: hoist everything to the root |
| node-linker=hoisted | No symlinks at all; a flat npm-like tree |
Prefer the narrowest fix: if only one plugin is not found, add it to public-hoist-pattern (or simply install it as a direct dependency) instead of hoisting everything.
Making it permanent
# .npmrc (pnpm 9 and older)
shamefully-hoist=true# pnpm-workspace.yaml (newer releases read settings from here)
shamefullyHoist: trueAfter changing the setting, delete node_modules and run pnpm install so the layout is rebuilt.
When it is needed
- Old React Native, Expo or Electron setups whose bundlers resolve packages from the root only.
- Plugins that
require()peer packages they do not declare. - Tools that scan
node_modulesdirectly instead of using Node.js resolution.
When symlinks themselves are the problem (some Windows tools, serverless bundlers, Docker copies that drop links), use node-linker=hoisted instead. To see why a package is present at all, run pnpm why.
FAQ
Is shamefully-hoist the same as node-linker=hoisted?
No. shamefully-hoist keeps pnpm's symlinked .pnpm store and adds links for every package at the top of node_modules. node-linker=hoisted drops the symlinks and copies a flat tree like npm, which also helps tools that do not follow symlinks.
Why is it called shamefully?
Because it gives up one of pnpm's guarantees on purpose: with everything hoisted, code can import packages it never declared, the phantom dependencies pnpm normally prevents.
Where do I set shamefully-hoist permanently?
In .npmrc (shamefully-hoist=true) for pnpm 9 and older, or as shamefullyHoist: true in pnpm-workspace.yaml for newer releases. Run pnpm install again after changing it.