Skip to content

/flags/pnpm/pnpm-install-shamefully-hoist

pnpm install --shamefully-hoist: a flat node_modules for tools that need it

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
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:

bash
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

SettingResult
(default)Direct dependencies at the top; everything else only in .pnpm
public-hoist-patternHoist only matching packages to the root (default includes eslint and prettier plugins in older releases)
hoist-patternHoist into the hidden .pnpm/node_modules, visible to packages but not to your code
shamefully-hoist=trueSame as public-hoist-pattern=*: hoist everything to the root
node-linker=hoistedNo 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

ini
# .npmrc (pnpm 9 and older)
shamefully-hoist=true
yaml
# pnpm-workspace.yaml (newer releases read settings from here)
shamefullyHoist: true

After 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_modules directly 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.

Sources