Run npm install with dependency lifecycle scripts disabled by default, then rebuild only the packages you explicitly trust.
safe-install is for npm projects that want trusted dependency installs without
switching package managers.
npm lifecycle scripts can run arbitrary code during install. Setting
ignore-scripts=true blocks that whole class of install-time execution, but it
also breaks packages that legitimately need postinstall, install, or
preinstall scripts to build native bindings, download binaries, or finish
setup.
This package keeps the default install locked down and moves script execution
behind a reviewed allowlist in package.json.
- Add this to
.npmrc:
ignore-scripts=trueOptionally enable (requires npm v11.10.0+):
min-release-age=3
allow-git=root- Add script to
package.json:
{
"scripts": {
"safe-install": "npx -y @gkiely/safe-install@0.1.33"
}
}- Find dependencies that declare install-time scripts:
npm run safe-install -- review-deps- Review the output, then add trusted packages to
package.json. You can also enableblockExoticSubDepsas a lockfile-level backstop for transitive dependencies that point outside the npm registry withgit:,file:,link:, or remote tarball URL specifiers.
{
"blockExoticSubDeps": true,
"trustedDependencies": [
"esbuild",
"sharp"
]
}- Use
safe-installfor future installs:
npm run safe-install- If your project defines its own install lifecycle scripts,
safe-installruns them after dependency installation:
{
"scripts": {
"preinstall": "node scripts/preinstall.js",
"install": "node scripts/install.js",
"postinstall": "node scripts/setup.js"
}
}You can pass npm install args through:
npm run safe-install left-pad@latest
npm run safe-install --save-dev left-pad@latestYou can run npm update through the same command:
npm run safe-install updatesafe-install runs npm install with scripts blocked, then runs install scripts only for packages listed in
trustedDependencies.
It also runs your project's own preinstall, install, and postinstall
scripts when they are defined in the root package.json.
If blockExoticSubDeps is set to true in package.json, safe-install also
fails the install before rebuilding trusted dependencies when a transitive
dependency points outside the npm registry with a git:, file:, link:, or
remote tarball URL specifier.
Equivalent manual flow:
npm install --ignore-scripts
npm rebuild --ignore-scripts=false esbuild sharp
npm run --ignore-scripts --if-present preinstall
npm run --ignore-scripts --if-present install
npm run --ignore-scripts --if-present postinstallOnly add a package to trustedDependencies after reviewing why it needs an
install script. This does not make dependency scripts safe; it makes the trust
decision explicit and version-controlled.
Alternatively, you can generate the scripts directly in your own repo:
npx -y @gkiely/safe-install@0.1.33 initThis adds the same scripts to your package.json (safe-install, review-deps, rebuild-trusted-dependencies) as well as scripts/review-deps.mjs, so future installs use your local scripts instead of calling npx -y @gkiely/safe-install.
- Add
ignore-scripts=trueandmin-release-age=3to.npmrc. - Create 3 scripts in
package.json:
{
"scripts": {
"safe-install": "npm install && npm run rebuild-trusted-dependencies && npm run --if-present preinstall && npm run --if-present install && npm run --if-present postinstall",
"review-deps": "node scripts/review-deps.mjs",
"rebuild-trusted-dependencies": "npm rebuild --ignore-scripts=false $(node -p \"require('./package.json').trustedDependencies.join(' ')\")"
}
}- Create
scripts/review-deps.mjs:
import { readFileSync } from 'node:fs';
/**
* @typedef {{ hasInstallScript?: boolean }} LockPackage
* @typedef {{ packages?: Record<string, LockPackage> }} PackageLock
*/
/** @type {PackageLock} */
const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));
/** @type {Set<string>} */
const names = new Set();
for (const [path, pkg] of Object.entries(lock.packages ?? {})) {
if (!pkg.hasInstallScript) continue;
const [, name] = path.match(/^node_modules\/(@[^/]+\/[^/]+|[^/]+)/) ?? [];
if (name) names.add(name);
}
console.log([...names].sort().join('\n'));- Run:
npm run review-depsand add trusted dependencies topackage.json:
{
"trustedDependencies": [
"esbuild",
"sharp"
]
}