Security model
npm, pnpm, and yarn all execute preinstall, install, and postinstall scripts silently
during installation. A malicious package can read your SSH keys, exfiltrate .env files, or
install a backdoor — all during a simple npm install. sandboxpm operates on a zero-trust
principle instead:
- No package has filesystem or network access during installation unless explicitly granted.
- Scripts are never executed silently — every script requires developer consent.
- Approved scripts run in ephemeral Docker containers with no access to host credentials, SSH
keys, or environment variables (beyond an explicit, minimal
envPassthroughallowlist). - All tarballs are verified against the SHA-512 published by the registry before extraction — never after.
The install-script prompt
When sandboxpm finds a package with a lifecycle script, you see exactly what it is before anything runs:
──────────────────────────────────────────────────────
⚠ 3 packages have install scripts
──────────────────────────────────────────────────────
1. esbuild@0.19.4
Type: postinstall
Script: node install.js
Inspect: https://unpkg.com/esbuild@0.19.4/install.js
Run this script? [y/N/inspect/always/never]
.sandboxpmrc's policies.onWarn / policies.onBlock settings let a team pre-decide how sandbox
warnings and blocks are handled in CI (abort, prompt, or continue), and whitelist/
blacklist entries let you pre-approve or permanently ban specific packages — see
Configuration.
Typosquat & package-risk checks
The install-script prompt only helps once a malicious package is already being installed. Before
that, during dependency resolution, every package name is checked against a bundled snapshot of
popular npm package names using edit-distance (so lodahs, epxress, or expres5 all get
caught as look-alikes of lodash/express). A name-similarity match is escalated to high
severity when the matched package is also newly published (under 90 days old, or has very few
released versions) or has a single maintainer — signals pulled from data the resolver already
has in hand, with no extra network calls.
──────────────────────────────────────────────────────
⚠ lodahs@1.0.0 looks risky
──────────────────────────────────────────────────────
Severity: high
Reason: typosquat:lodash(distance=1)
Reason: new-package(12 days old)
Proceed with this package? [y/inspect/always/never]
.sandboxpmrc's policies.onPackageRisk (continue/prompt/abort) controls this the same way
onWarn controls the script prompt. trustedPackages exempts specific names from the check
permanently (e.g. an internal fork); blockedPackages is a hard blocklist — any match aborts
resolution before a single tarball is downloaded, and it applies even to installs from an
existing sandboxpm.lock, since it's a pure name comparison. Manage both lists with
sandboxpm trust add/remove and sandboxpm block add/remove, or edit .sandboxpmrc directly —
see Configuration.
Known limits: there's no live CVE/advisory feed and no live npm download-count signal (both would
require an ongoing external data dependency); the bundled popular-package list is a manually
curated snapshot, not an automatically refreshed top-N export; and a lockfile-based install
(sandboxpm.lock already fresh) skips the typosquat/low-trust scan entirely, since it never
contacts the registry — only the blockedPackages hard blocklist still applies there.
Content-addressable store
Every file from every package is stored once in ~/.sandboxpm/store/, keyed by its SHA-512 hash.
When a package is installed into a project, its files are hard-linked from the store — never
duplicated across projects:
~/.sandboxpm/store/
└── sha512-{hash} ← one file, shared across all projects via hard links
project-a/node_modules/express → hard link to store
project-b/node_modules/express → same hard link, zero extra disk space
Non-flat node_modules
Only direct dependencies appear in the root node_modules/. Transitive dependencies live nested
under node_modules/.sandboxpm/. This prevents phantom dependency access: your code can only
import what you explicitly declared in package.json.
Docker sandbox
Approved scripts run through dockerode in a container that is:
- read-only, with
CapDrop: ALL - restricted by an explicit seccomp syscall allowlist (Linux) or Hyper-V isolation (Windows)
- attached to an isolated bridge network with no host credential or network access by default
A native (unsandboxed) fallback exists for sandbox-start failures or Linux-ELF/Windows-host native-addon mismatches, but it requires an explicit double-confirmation — sandboxpm never falls back to running a script unsandboxed silently. Full detail on the sandbox images and syscall policy lives in Architecture.