Isolated installs

Strict dependency isolation similar to pnpm's approach

Bun provides an alternative package installation strategy called isolated installs that creates strict dependency isolation similar to pnpm's approach. This mode prevents phantom dependencies (packages importing dependencies they never declared) and makes builds reproducible and deterministic.

Isolated installs are the default for new workspace/monorepo projects (with configVersion = 1 in the lockfile). Existing projects continue using hoisted installs unless explicitly configured.

What are isolated installs?#

Isolated installs create a non-hoisted dependency structure where packages can only access their explicitly declared dependencies. This differs from the traditional "hoisted" installation strategy used by npm and Yarn, where dependencies are flattened into a shared node_modules directory.

Key benefits#

  • Prevents phantom dependencies — Packages cannot accidentally import dependencies they haven't declared
  • Deterministic resolution — Same dependency tree regardless of what else is installed
  • Better for monorepos — Workspace isolation prevents cross-contamination between packages
  • Reproducible builds — More predictable resolution behavior across environments

Using isolated installs#

Command line#

Use the --linker flag to specify the installation strategy:

terminal
# Use isolated installs
bun install --linker isolated

# Use traditional hoisted installs
bun install --linker hoisted

Configuration file#

Set the default linker strategy in your bunfig.toml or globally in $HOME/.bunfig.toml:

bunfig.toml
[install]
linker = "isolated"

Default behavior#

The default linker strategy depends on your project's lockfile configVersion:

configVersionUsing workspaces?Default Linker
1✅isolated
1❌hoisted
0✅hoisted
0❌hoisted

New projects: Default to configVersion = 1. In workspaces, v1 uses the isolated linker by default; otherwise it uses hoisted linking.

Existing Bun projects (made pre-v1.3.2): If your existing lockfile doesn't have a version yet, Bun sets configVersion = 0 when you run bun install, preserving the previous hoisted linker default.

Migrations from other package managers:

  • From pnpm: configVersion = 1 (using isolated installs in workspaces)
  • From npm or yarn: configVersion = 0 (using hoisted installs)

Override the default by passing the --linker flag or setting it in your configuration file.

How isolated installs work#

Directory structure#

Instead of hoisting dependencies, isolated installs create a two-tier structure:

tree layout of node_modules
node_modules/
├── .bun/                          # Central package store
│   ├── node_modules/              # Hoisted fallback (see install.hoist)
│   │   └── package -> ../package@1.0.0/node_modules/package
│   ├── package@1.0.0/             # Versioned package installations
│   │   └── node_modules/
│   │       ├── .bin/              # Executables of the package and its dependencies
│   │       ├── package/           # Actual package files
│   │       └── dep -> ../../dep@2.0.0/node_modules/dep  # One symlink per dependency
│   ├── @scope+package@2.1.0/      # Scoped packages (+ replaces /)
│   │   └── node_modules/
│   │       └── @scope/
│   │           └── package/
│   ├── package@1.0.0+f8a822eca018d0a1/  # The same version with other peer dependencies
│   └── ...
└── package-name -> .bun/package@1.0.0/node_modules/package  # Symlinks

The part of a store directory name after @ is the package's resolution. For a registry package, that is its version. For a folder, tarball, git, or GitHub dependency, Bun derives it from the path or URL (plus the commit), so it can be long. Bun writes at most 80 bytes of it: it cuts a longer resolution to at most 63 bytes and appends + and 16 hex digits derived from the full resolution. The path of a package inside the store is therefore at most 34 + 2 * <name length> + 80 characters longer than your project directory (17 more when the package has peer dependencies or is a tarball). This matters on Windows, where a path longer than 260 characters still works for Bun itself, but not as the working directory of the package's lifecycle scripts.

In the name and the resolution, /, \, :, # and ? become +:

DependencyStore directory
npm registryis-number@7.0.0, @types+react@18.2.0
folder (file: directory)local-pkg@file+vendor+local-pkg
local tarballbar@.+bar-0.0.2.tgz+3f2a9c01d47be650
remote tarballbar@https+++example.com+bar-0.0.2.tgz+3f2a9c01d47be650
Gitpkg@git+https+++github.com+user+repo.git+<commit>
GitHubpkg@github+user+repo+<commit>
with peer dependenciesreact-dom@18.2.0+f8a822eca018d0a1
  • The directory inside is named after the package's own name, the symlinks to it after the name each dependent uses (the alias, for an npm: alias).
  • A tarball's directory ends in the first 16 hex digits of the hash that bun.lock records for it, so another build at the same path or URL gets another directory.
  • Credentials and the query string of a URL are left out. 16 hex digits derived from the full URL take their place.
  • The root package, workspace packages and link: dependencies have no store directory. Dependents link to their folders.

This describes the current layout. It can change between Bun versions.

Resolution algorithm#

  1. Central store — Bun installs all packages in node_modules/.bun/package@version/ directories
  2. Symlinks — Top-level node_modules contains symlinks pointing to the central store
  3. Peer resolution — Complex peer dependencies create specialized directory names
  4. Deduplication — Packages with identical package IDs and peer dependency sets are shared
  5. Re-linking — On later installs, Bun reuses existing store entries and re-points their symlinks if a dependency was re-resolved. The name and version are part of the entry's directory name, so Bun reuses an entry when it exists and does not read the package.json in it. Run bun install --force to rebuild every entry. Store entries that are no longer referenced stay until you run bun prune

Workspace handling#

In monorepos, Bun handles workspace dependencies specially:

  • Workspace packages — Symlinked directly to their source directories, not the store
  • Workspace dependencies — Can access other workspace packages in the monorepo
  • External dependencies — Installed in the isolated store
  • Lifecycle scripts — A workspace package's scripts run after every dependency is installed, so they can use the root's dependencies and executables

Comparison with hoisted installs#

AspectHoisted (npm/Yarn)Isolated (pnpm-like)
Dependency accessPackages can access any hoisted dependencyPackages only see declared dependencies
Phantom dependencies❌ Possible✅ Prevented
Disk usage✅ Lower (shared installs)✅ Similar (uses symlinks)
Determinism❌ Less deterministic✅ More deterministic
Node.js compatibility✅ Standard behavior✅ Compatible via symlinks
Best forSingle projects, legacy codeMonorepos, strict dependency management

Advanced features#

Peer dependency handling#

Isolated installs encode peer dependencies in the store path:

tree layout of node_modules
# Package with peer dependencies creates specialized paths
node_modules/.bun/package@1.0.0+f8a822eca018d0a1/

The directory name ends in a hash of the packages its peer dependencies resolved to, so each unique combination gets its own installation.

Strict resolution with install.hoist = false#

By default, Bun creates node_modules/.bun/node_modules, a fallback directory with a symlink for every dependency name (the alias, for a package installed through an npm: alias). Because this directory is an ancestor of every store entry, a package in the store can resolve dependencies it never declared. Set install.hoist = false (or hoist=false in .npmrc, matching pnpm) to skip creating this directory. Undeclared imports then fail instead of depending on what else happens to be installed. One caveat, shared with pnpm: the root node_modules also sits above the store. As a result, packages linked there (your direct dependencies, publicHoistPattern matches, and workspace packages) stay resolvable from any store package:

bunfig.toml
[install]
linker = "isolated"
hoist = false

install.hoistPattern and install.publicHoistPattern offer pattern-based control over the same fallback directory and the root node_modules respectively.

Global virtual store#

When install.globalStore is enabled, Bun materializes store entries once into a global virtual store at <cache>/links/, and node_modules/.bun/<pkg>@<ver> is a symlink into it. Warm installs after rm -rf node_modules only create one symlink per package instead of copying every package's files again, which is roughly 7× faster on a typical mid-size project. The global store is off by default; see the global store docs for how to enable it, the full layout, benchmarks, and tradeoffs.

Backend strategies#

When the global store is disabled (the default) or an entry isn't eligible for it, Bun materializes the entry under the project using one of:

  • Clonefile (macOS) — Copy-on-write filesystem clones
  • Hardlink (Linux/Windows) — Hardlinks to save disk space
  • Copyfile (fallback) — Full file copies when other methods aren't available

Debugging isolated installs#

Enable verbose logging to see what an install is doing:

terminal
bun install --linker isolated --verbose

The verbose output shows:

  • Store entry creation
  • Symlink operations
  • Peer dependency resolution
  • Deduplication decisions

Troubleshooting#

Compatibility issues#

Some packages may not work correctly with isolated installs due to:

  • Hardcoded paths — Packages that assume a flat node_modules structure
  • Dynamic imports — Runtime imports that don't follow Node.js resolution
  • Build tools — Tools that scan node_modules directly

If you encounter issues, you can:

  1. Switch to hoisted mode for specific projects:

    terminal
    bun install --linker hoisted

    Switching linkers in either direction reinstalls the existing node_modules from scratch. Dot-directories of other tools, like .cache and .vite, are kept. Those of a package manager, like .pnpm, are removed. The previous tree is moved to node_modules/.old_modules-<random> and deleted once the install has succeeded. bun prune removes the ones that a failed install or an older version of Bun left behind.

    Bun installs through a node_modules that is a symlink (the project's or a workspace's), but clears nothing behind the link and prints a warning. Empty the folder yourself before you switch.

  2. Report compatibility issues to help improve isolated install support

Performance considerations#

  • Install time — May be slightly slower due to symlink operations
  • Disk usage — Similar to hoisted (uses symlinks, not file copies)
  • Memory usage — Higher during install due to complex peer resolution

Migration guide#

From npm/Yarn#

terminal
# Remove existing node_modules and lockfiles
rm -rf node_modules package-lock.json yarn.lock

# Install with isolated linker
bun install --linker isolated

From pnpm#

Isolated installs are conceptually similar to pnpm, so migration is direct:

terminal
# Remove pnpm's node_modules
rm -rf node_modules

# Install with Bun's isolated linker
bun install --linker isolated

Keep pnpm-lock.yaml around for this first install: Bun converts it to bun.lock, so you keep the versions pnpm had resolved. The same first install (no bun.lock yet, no workspaces field in the root package.json) also moves the workspace list, catalogs and overrides from pnpm-workspace.yaml into the root package.json, with or without a pnpm-lock.yaml. See pnpm migration for what is converted.

The layouts are close: Bun hardlinks (clones on macOS) packages from its global cache into a per-project store, node_modules/.bun/, and symlinks top-level node_modules entries into it. With install.globalStore enabled, those store entries become symlinks into a global virtual store instead.

When to use isolated installs#

Use isolated installs when:

  • Working in monorepos with multiple packages
  • Strict dependency management is required
  • Preventing phantom dependencies is important
  • Building libraries that need deterministic dependencies

Use hoisted installs when:

  • Working with legacy code that assumes flat node_modules
  • Compatibility with existing build tools is required
  • Working in environments where symlinks aren't well supported
  • You prefer the traditional npm behavior