Skip to main content
meow produces real node_modules directories — not a virtual filesystem and not symlinked package contents. That’s a deliberate choice: V8, Vite, and bundlers resolve through fs.realpath, and symlinked package contents break that. meow gives you the speed of a shared store with the compatibility of real files.

The layout

Packages are materialized into a project-local content-addressed store under node_modules/.meow, and the top-level names you import are edges pointing into it:
node_modules/
├── .meow/
│   ├── react@19.0.0/
│   │   └── node_modules/
│   │       └── react/          ← real files (copy-on-write from the cache)
│   └── scheduler@0.25.0/
│       └── node_modules/
│           └── scheduler/
├── react/      → edge into .meow/react@19.0.0/node_modules/react
└── scheduler/  → edge into .meow/scheduler@0.25.0/node_modules/scheduler
This is a flat, deduplicated, pnpm-style graph — but with a crucial difference in how the bytes get there.

Contents are cloned, not linked

The actual package files come from the global cache using the fastest primitive each OS offers:
PlatformPackage contentsDependency edges
macOS (APFS)clonefile — O(1) copy-on-writesymlinks
Linuxrecursive hardlinkssymlinks
Windowsrecursive hardlinksNTFS directory junctions
The result: 0 bytes of duplicated disk. A file shared across versions or projects exists once physically; copy-on-write means a write would fork it rather than corrupt the shared copy.
Why edges stay links but contents don’t. The pointer inside a package’s node_modules to another package (a graph edge) is a link — cheap, and traversal through it is fine. But a package’s own files are cloned/hardlinked, never symlinked, so tools that call fs.realpath (V8, Vite) see a normal directory and never trip on symlink traversal.

Incremental & skippable

meow writes a small .materialized sidecar that records the state of the tree. If node_modules is already up to date for the current lockfile, materialization is skipped entirely:
│ node_modules/ skipped (already up to date) │
A content hash of the materialized tree is computed so meow can detect drift and repair it on the next install.

Cleaning & rebuilding

meow install --clean        # remove node_modules first, then materialize fresh
Because the global cache holds the bytes, a clean rebuild is cheap — it re-clones from the cache rather than re-downloading.

Vendoring

For fully self-contained, reproducible trees (air-gapped builds, committed dependencies, deterministic CI), use vendor mode, which copies packages instead of linking them and normalizes file metadata for byte-reproducibility:
meow install --vendor                  # copy into vendor/
meow install --vendor --vendor-dir deps  # custom directory
ModeContentsMetadataUse when
node_modules (default)copy-on-write / hardlinkas-isday-to-day development
vendorfull copiesnormalized (fixed mtimes)reproducible/air-gapped builds

Run package binaries

meow x, global installs, and searching the registry.