npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jboho/safe-pnpm

v2026.10.10

Published

Docker-based pnpm install isolation — protects developer machines from malicious npm lifecycle scripts

Readme


Overview

safe-pnpm wraps pnpm, npm, and yarn so that package lifecycle scripts (postinstall, preinstall, etc.) never run on your machine.

When you run pnpm install, npm install, or yarn install, the wrapper pre-scans for known CVEs and malicious packages, runs the install inside an ephemeral Docker container that only sees your manifests and lockfile (never source files, .env, or credentials), then copies node_modules back. All non-install commands pass through instantly with no overhead.

| Topic | Links | | ------------------- | ------------------------------------------------------------------ | | License | MIT | | Security model | docs/security.md · SECURITY.md | | Contributing | CONTRIBUTING.md | | Roadmap | ROADMAP.md | | Socket scanning | docs/socket.md | | Private registries | docs/private-registry.md | | Performance | docs/performance.md | | Versioning | CalVer YYYY.M.MICRO |

Status

Published to npm as @jboho/safe-pnpm (first public release 2026.10.2, with provenance). Known gaps, tracked in ROADMAP.md under M4:

  • On Windows the PowerShell wrappers still run the containers as root inside Docker (--cap-drop ALL and no-new-privileges apply). macOS and Linux run them as your user.
  • Rootless Docker and Podman are detected from docker info, and there the containers keep their default user, which is your own user on the host. This is not tested against a real rootless daemon.
  • Unless your uid is 1000 (the image's node user), it has no user name inside the image, so a build script that looks up the current user (whoami, os.userInfo()) gets an error.
  • The CVE audit runs on the host after the fetch, on a throwaway copy of the resolved manifests and lockfile plus your registry config. It does not run in your project directory, and pnpm skips .pnpmfile.cjs for it. A yarn-path line in .yarnrc is dropped for the audit.
  • PowerShell is the least-tested shell: the wrappers and copy-back are tested on Linux pwsh only.
  • PowerShell removes an unquoted -- before the wrapper function sees it, so npm run build -- --watch reaches npm as npm run build --watch. Quote it to keep it: npm run build '--' --watch.
  • In PowerShell a refusal is a terminating error (safe-pnpm: command refused), so && chains and scripts stop there; catch it with try/catch if a script should go on. A command the wrapper passes to the real manager sets $LASTEXITCODE but leaves $? true even when it fails, as with any PowerShell function, so check $LASTEXITCODE rather than && after a wrapped command.
  • Requires Docker; developed and tested on macOS and Linux CI. On Windows only the PATH shims run in CI (windows-latest); the rest is not tested there.

Install

npm install -g @jboho/safe-pnpm
safe-pnpm setup

Requires: Docker Desktop 4.0+, Node.js 16+, and at least one of: pnpm 7.0+, npm 7.0+, yarn 1.x.

The package is named safe-pnpm for historical reasons. npm and yarn are first-class — setup detects which managers you have installed and lets you choose which ones to wrap.


Commands

safe-pnpm setup

Interactive first-time installation. Checks prerequisites, builds the safe-pnpm:latest Docker image, and wires shell functions for each manager you enable.

safe-pnpm setup
# Detected package managers:
#   Enable pnpm   (10.33.0)  [Y/n]
#   Enable npm    (10.2.4)   [Y/n]
#   Enable yarn   (1.22.22)  [Y/n]

source ~/.zshrc    # or open a new terminal

safe-pnpm update

Refresh wrapper files and rebuild the Docker image after a package update. If update detects a manager that wasn't enabled before, it offers to add it:

command npm update -g @jboho/safe-pnpm
safe-pnpm update

If you turned on global PATH shims (safe-pnpm setup --path-shims), command npm reaches the npm shim and is refused too; run npm by its absolute path instead (safe-pnpm doctor lists it, and the refusal message prints it). After upgrading, run safe-pnpm update before safe-pnpm setup --path-shims; setup refuses to install shims next to wrapper files from another release.

Use command npm here. The wrappers refuse -g and --global installs, because the sandbox would install into a container that is thrown away and change nothing on your machine. Add command in front (command npm install -g PKG) to run a global install natively, without the safety checks.

safe-pnpm exec -- CMD

Run a program with the safe-pnpm PATH shims first on PATH, for it and everything it starts:

safe-pnpm exec -- nx mcp

The wrappers are shell functions, so a program that starts pnpm, npm, npx, pnpx or yarn itself (an editor, an MCP server, child_process.spawn("pnpm")) skips them. The shims in ~/.safe-pnpm/bin run the same checks for those programs. exec passes the arguments through unchanged and exits with the program's exit code.

To put the shims first on PATH in every new shell instead (opt-in), run safe-pnpm setup --path-shims; safe-pnpm setup --remove-path-shims undoes it. What the shims cover and what they don't (absolute paths, npm_execpath, corepack, Dock-launched apps, CI; on Windows the .cmd/.ps1 shims need PowerShell 7, --path-shims edits only the PowerShell profile, and cmd.exe rewrites some arguments), plus MCP server recipes: docs/security.md.

safe-pnpm doctor

Check that everything is installed and configured correctly:

  ✓  Docker           running
  ✓  Image            safe-pnpm:latest present
  ✓  Installation     ~/.safe-pnpm/ present
  ✓  Wrapper files    all 9 files present
  ✓  Shell config     source line found in ~/.zshrc
  ✓  pnpm             10.33.0
  ✓  npm              10.2.4
  ✓  Node.js          v22.0.0
  ✓  CA cert          not configured (no TLS proxy)
  ✓  PATH shims       pnpm, pnpx, npm, npx in ~/.safe-pnpm/bin
  ✓  Shims on PATH    not on PATH (scoped: use safe-pnpm exec -- CMD, or setup --path-shims)

  Package managers on PATH, in lookup order
  pnpm
        ~/Library/pnpm/pnpm
  ...

How It Works

When you run an install-class command:

| Manager | Intercepted subcommands | |---|---| | pnpm | install, add, update, ci, remove, fetch | | npm | install, i, ci, update, uninstall, un | | yarn | install, add, remove, upgrade (yarn v1 only) |

Flags that point an install at another directory are refused. The checks read the lockfile in the directory you run the command from, so an install sent elsewhere would go unchecked. On an intercepted command the wrappers refuse npm's --prefix and -C, pnpm's -C, --dir, --prefix and --lockfile-dir (also as --config.dir= and similar), and yarn's --cwd, in any position, in the --flag=DIR form and in the shortened or single-dash spellings the managers accept. cd into that directory and run the command there, or put command in front (command pnpm -C web install) to run it natively without the safety checks. Commands that aren't intercepted, such as pnpm -C web run build, are not affected. pnpm installs are also refused when the project's .npmrc sets lockfile-dir or its pnpm-workspace.yaml sets lockfileDir; remove the setting to install through the wrapper. They are refused too when shared-workspace-lockfile is false (in .npmrc, as sharedWorkspaceLockfile in pnpm-workspace.yaml, or on the command line), since each workspace member would keep its own lockfile and there is no root lockfile to scan; set it back to true to install through the wrapper.

-- is refused on intercepted commands. The sandbox adds --ignore-scripts and its other flags after your arguments. After -- (or ---, which npm and pnpm read the same way) the managers would take those flags as package names, and install scripts would run in the fetch phase, which has your registry token and the network. Drop the --, or put command in front to run it natively without the safety checks. Commands that aren't intercepted, such as npm run test -- --watch, get the -- unchanged.

Download-and-run commands get a malware check only. These fetch a package and run it straight away, so they skip the Docker sandbox, the CVE audit and Socket, and the package's code runs on your machine with your permissions:

  • npx PKG, npm exec PKG, npm init PKG, npm create PKG
  • pnpm dlx PKG, pnpx PKG, pnpm create PKG
  • yarn create PKG, and yarn dlx PKG on yarn 2+

In bash, zsh, fish and PowerShell (not yet tested on native Windows) the wrappers look up the version the command would fetch (npm view, which reads registry metadata and runs no package code), check that name@version against OSV's malicious-package advisories, and refuse to run the command on a hit. A create command checks the create- package the manager would fetch. If OSV or the lookup fails the command warns and runs; SAFE_PNPM_OSV_STRICT=1 blocks instead. Paths, URLs, git specs and packages in private scopes are not checked. npx is wrapped only when the npm wrapper is enabled, and pnpx only with the pnpm wrapper. OSV only knows packages already reported as malicious, so treat anything new with care and check the name yourself.

  1. Pre-install scan — Socket behavioral analysis if configured.
  2. Copy only manifests — package.json, the lockfile, workspace files, and .npmrc / .yarnrc go into a temp directory. Source files, .env, and secrets never leave the host.
  3. Fetch in Docker — ephemeral container running as your user, --cap-drop ALL, --ignore-scripts. Dependencies download with any registry token available but no package code running.
  4. Malware check — every version the fetch resolved, including packages being added, is checked against OSV's malicious-package advisories. A hit stops the install before any package code runs. If OSV can't be reached the install warns and continues; export SAFE_PNPM_OSV_STRICT=1 blocks instead. Packages in scopes your .npmrc/.yarnrc maps to a private registry are not sent to OSV. The manager's CVE audit (pnpm audit, npm audit, yarn audit) runs next, on the resolved tree, so a package passed to add is audited too. See the two-phase model.
  5. Build in Docker — a second container with --network none and the token removed runs any lifecycle/build scripts against the downloaded store — nothing to steal, nowhere to send it. See the two-phase model.
  6. Copy results back — node_modules lands in your project; the lockfile and manifests come from a snapshot taken before any build script ran. The containers are discarded.

Socket.dev Scanning

Socket adds a third layer — behavioral analysis of package lifecycle scripts (suspicious network calls, obfuscation, typosquatting) — alongside the always-on CVE audit and OSV malware check. It is opt-in: authenticate once with socket login, then enable it per-command or per-session.

pnpm install --socket              # this invocation only
npm install --socket
yarn add lodash --socket

export SAFE_PNPM_ENABLE_SOCKET=1   # every install-class command this session

Put --socket after the subcommand (pnpm install --socket, not pnpm --socket install) — safe-pnpm consumes the flag before the args reach the package manager.

A scan that finds bad packages prompts (interactive) or warns and continues (non-interactive); a scan that couldn't run — no token, offline, quota exhausted — only warns, so an expired token never silently blocks you. In CI, set SAFE_PNPM_SOCKET_STRICT=1 to turn both findings and scan failures into hard blocks; without it a pipeline whose Socket token has expired stays green while doing no behavioral scanning at all.

What each outcome looks like on the console (the scan runs before the Docker install):

→ Socket behavioral scan...
# healthy    — nothing else printed; the install proceeds
# findings   — ⚠️  Socket reported policy violations for this dependency set. Continuing in non-interactive mode.
# scan failed — ⚠️  Socket scan could not run: 401 Unauthorized. Continuing without Socket results.
# under SAFE_PNPM_SOCKET_STRICT=1, either warning becomes:
#             ✗ ... Blocking (SAFE_PNPM_SOCKET_STRICT=1).
export SAFE_PNPM_SOCKET_STRICT=1   # CI: block on findings AND on scan failure

The Socket layer shells out to a bundled Node classifier, so node must be on PATH at install time for it to run (the base install already requires Node.js 16+). If node is missing the layer is treated as a failure — warned by default, blocked under strict mode.

See docs/socket.md for the full outcome table, failure semantics, and account tiers.


Strict mode (CI)

By default, a scan that could not run (offline, expired token, audit error) warns and the install continues. In CI, set one variable to block instead:

export SAFE_PNPM_STRICT=1   # block on audit, Socket and OSV findings or failures
export SAFE_PNPM_MEMORY=2g  # optional: memory limit for both containers

SAFE_PNPM_OSV_STRICT=1 and SAFE_PNPM_SOCKET_STRICT=1 set the same behavior for a single layer.


Native optional dependencies on macOS

Packages such as rollup and esbuild ship one native build per platform as optional dependencies. The install runs in a Linux container, so it would pick only the Linux build, and tools you run on the Mac afterwards fail with Cannot find module @rollup/rollup-darwin-arm64.

On a Mac or Windows host the wrapper also fetches the Mac build, and it goes through the same malware check and CVE audit as everything else:

  • pnpm — the sandbox copy of the project gets supportedArchitectures for the host: in pnpm-workspace.yaml when the project pins no pnpm or pnpm 11 or later, and in the pnpm field of package.json when packageManager or devEngines.packageManager pins pnpm 10 or earlier, or a version or range the wrapper can't place (such as pnpm@latest or >=9). pnpm before 10.5 rejects a workspace file without packages, and pnpm 8 and 9 don't read the setting there. For those projects the image's pnpm 11 warns that it ignores pnpm.supportedArchitectures before it hands off to the pinned pnpm, which reads it. Your own files are not changed (a pnpm add keeps its new dependency, without the setting), and a supportedArchitectures you already set is kept. This needs pnpm-arch.js in ~/.safe-pnpm/; run safe-pnpm update if the wrapper warns that it is missing.
  • yarn — --ignore-platform, which fetches the builds for every platform (yarn 1 cannot name one).
  • npm — after the fetch, the wrapper installs the lockfile's optional packages built for the host next to the Linux ones (npm install --no-save --ignore-scripts), by name and version from the lockfile that was just malware-scanned. --os/--cpu can't do this: they replace the container's platform, which breaks esbuild's install script in the build container. This needs host-specs.js in ~/.safe-pnpm/; run safe-pnpm update if the wrapper warns that it is missing.

Known limit for npm: the extra install fetches name@version from the registry again and does not check the lockfile's integrity hash. A nested lockfile entry (inside another package's node_modules) is installed at the top level instead. The malware scan and CVE audit cover the same name and version that gets installed.

On Windows the extra build is win32. Running command pnpm install to get around this skips the malware scan; use the above instead.


Bypass

To bypass the wrapper and run the native binary directly:

command pnpm install   # bash, zsh and fish
command npm install
command yarn install

# With setup --path-shims, `command pnpm` reaches the shim; run the real
# binary by absolute path instead (safe-pnpm doctor lists them).

# PowerShell. With setup --path-shims, pnpm.cmd is the shim; run the
# real binary by absolute path instead.
pnpm.cmd install
npm.cmd install
yarn.cmd install

Private Registries

Private and scoped registries work out of the box. Map the scope in the project's .npmrc, put the auth line in your ~/.npmrc with the token as a variable (//npm.acme.example/:_authToken=${NPM_TOKEN}), and install normally:

export NPM_TOKEN=your-read-only-token   # or NODE_AUTH_TOKEN
pnpm install

The wrapper copies only the ~/.npmrc auth lines whose value is exactly ${NODE_AUTH_TOKEN} or ${NPM_TOKEN} into the fetch phase; literal tokens stay on the host. pnpm 11 ignores a ${NPM_TOKEN} auth line in the project .npmrc.

The token is available only during the script-free fetch phase and is withheld (along with any .npmrc credential lines) during the build phase, so untrusted lifecycle scripts never see it. The old SAFE_PNPM_FORWARD_TOKENS opt-in is obsolete. See private-registry.md.

If a package's build step genuinely needs network (e.g. esbuild, sharp), set SAFE_PNPM_BUILD_NETWORK=1; the token is still withheld.


Docker Not Running

When Docker is unavailable, the wrapper fails closed rather than silently running the native package manager (which would execute untrusted lifecycle scripts on the host):

  • Interactive shell — you are prompted before any native fallback.
  • Non-interactive (CI, scripts) — the install is refused. To allow a native fallback in this case, set SAFE_PNPM_ALLOW_NATIVE_FALLBACK=1.

Shell Support

| Shell | Config modified | |---|---| | zsh | ~/.zshrc | | bash (macOS) | ~/.bash_profile | | bash (Linux) | ~/.bashrc | | fish | ~/.config/fish/functions/{pnpm,npm,yarn}.fish (one per enabled manager), plus npx.fish with npm and pnpx.fish with pnpm | | PowerShell | ~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 |


Further Reading