pnpm's nodeLinker setting defaults to isolated, which symlinks dependencies from a virtual store at node_modules/.pnpm. Setting it to hoisted builds a flat node_modules like npm or Yarn Classic. Isolated limits what your code can import by accident. Hoisted trades that for compatibility with tools that dislike symlinks.
The values
The pnpm docs describe nodeLinker as the linker used to install Node packages. The options are isolated, hoisted, pnp, loaded or a loaded-linker object. The two most common:
| Value | Layout | When to choose it |
|---|---|---|
| isolated (default) | Dependencies symlinked from a virtual store at node_modules/.pnpm | Most projects |
| hoisted | Flat node_modules without symlinks, like npm or Yarn Classic | Tooling that fails with symlinks, some serverless hosts, publishing with bundledDependencies, running Node with the preserve-symlinks flag |
Those reasons for hoisted are the docs' own list. The docs also note that for serverless hosts that lack symlink support, bundling your application before deployment is an alternative.
The security angle: phantom dependencies
In a flat layout, your code can import packages you never declared, because a transitive dependency happens to sit at the top. In isolated layout, a package sees its declared dependencies. This reduces accidental reliance on code whose version you did not choose.
pnpm still has softening settings. By default hoist is true and hoists all dependencies into node_modules/.pnpm/node_modules, which makes unlisted dependencies accessible to packages inside node_modules, but not to your own code in the project root. The docs call this semistrict. publicHoistPattern moves matching packages to the root node_modules, so application code can reach them, and shamefullyHoist (default false) hoists everything to the root.
| Setting | Default | Effect on exposure |
|---|---|---|
| hoist | true | Dependencies reachable by other packages through a hidden folder |
| hoistPattern | all packages | Narrow it to only flawed packages that need it |
| publicHoistPattern | none | Moves matches to the root, exposing them to application code |
| shamefullyHoist | false | Hoists all to the root, the most exposure |
The docs recommend narrowing hoistPattern to exclusively hoist the packages known to have phantom dependency problems.
How to decide
- Start with isolated. Run your build and tests.
- If a tool breaks, find the cause. Often it imports something it never declared.
- Fix with the narrowest setting: add the missing dependency, or add one name to publicHoistPattern.
- Choose hoisted only for a documented reason, and write the reason in the repository.
- Re-run the install in CI to confirm both layouts behave.
Layout affects what runs, not which versions install. Version pinning still comes from the lockfile and your frozen install. See phantom dependencies and how pnpm blocks them, and what scanners read in a pnpm workspace.
What layout does not change
Isolated installs do not make a malicious package safe. Install scripts, a compromised release and a poisoned lockfile are separate risks with separate controls. Layout limits accidental access, not deliberate access: a malicious package can still read files on disk.
Where Safeguard fits
Safeguard's dependency scanning and SBOM generation read lockfiles whatever linker you use, and reachability analysis helps decide which vulnerable packages your code actually reaches. A free scan is at https://gold.safeguard.sh/scan.
Quick check
What is the default pnpm nodeLinker?
isolated, which symlinks dependencies from a virtual store at node_modules/.pnpm.
Does isolated mode stop malicious packages?
No. It limits accidental imports of undeclared packages. A package you declared can still run code, so script controls and review still matter.
Sources
Last reviewed: 19 October 2026.