Dependency management
npm dependencies are pinned by the committed package-lock.json, and installs
run only reviewed lifecycle scripts. For the threat model and rationale behind
these controls, see Supply-chain security.
Install contracts
CI, the devcontainer, and Netlify install lock-exact and script-free, then
explicitly re-enable the one reviewed hook: the hugo-extended rebuild that
fetches the pinned Hugo binary. Per environment:
- CI:
npm run ci:min; jobs that build the site follow withnpm run ci:prepare. - Devcontainer:
npm run install:safe, the same contract, keeping optional dependencies. - Netlify:
npm run install:safe, run by the Netlify build command after the inert auto-install, between clean-working-tree checks:- Lock drift or any other Git-visible change fails the build.
- If the check fails on residue from a retired path (Netlify’s build cache restores it), clear the deploy context’s build cache and retry rather than ignoring the path.
- Local:
npm run install:safe, or a standardnpm install, which follows the lock while it agrees withpackage.jsonand gates lifecycle scripts by the allowlist rather than disabling them; see local setup.
The nested Docsy theme setup follows the same contract: the prepare step
invokes Docsy’s own lock-exact, script-free theme-dependency install.
Updating dependencies
Routine updates
npm run update:packages bumps package.json only. The
release cooldown applies to the offered versions. Then
regenerate the lock and commit both files together:
npm install --package-lock-only --ignore-scripts
Script-bearing packages
When adding or updating a package that has, or needs, an allowScripts entry,
the contributor making the change:
- Reviews the new version’s lifecycle scripts.
- Records the outcome, committed together with the dependency change and vetted
in PR review: a needed script as an exact-version approval, an unneeded one
as a name-level denial (
false, which needs no update on later bumps). - For a new approval, also adds the package to the Renovate automerge exclusion
in
.github/renovate.json5: every bump of an approved package needs the steps above, so its update PRs must wait for a contributor.
Lock-file maintenance
- You changed dependencies: regenerate the lock as in
routine updates and commit it together with
package.json. - Merge conflict on the lock file: take the
mainversion and rerun the regeneration command. - The lock file changed, but you didn’t change dependencies (a
postinstallcheck warns when an install does this): that signals drift; restore the lock and investigate rather than committing the rewrite.
Supply-chain controls
Release cooldown
Version resolution ignores releases younger than the configured minimum age.
- Enforcement:
min-release-agein.npmrc. - Scope:
- Only resolving operations are affected; lock-exact installs (
npm ci) don’t resolve versions. - npm gives project config precedence over user config, so a stricter cooldown
in your user
.npmrcis relaxed to the project value here; to keep yours for an invocation, set thenpm_config_min_release_ageenvironment variable, which outranks both.
- Only resolving operations are affected; lock-exact installs (
- Renovate: applies its own cooldown to the update PRs it opens, set by
minimumReleaseAgein.github/renovate.json5; longer for the updates that merge without human review.
Lifecycle-script allowlist
Installs run a package’s lifecycle scripts only when its exact name and version
are listed in the allowScripts allowlist:
- Enforcement: the
allowScriptsmap inpackage.json, made fail-closed bystrict-allow-scriptsin.npmrc. - Denials:
- An entry set to
falserecords a reviewed denial: the package installs, its script is skipped. - Denials grant nothing, so they cover the package by name, across versions.
- An entry set to
- Interplay with
--ignore-scripts:- The allowlist only filters: it never re-enables scripts that
ignore-scriptsdisables, so script-free installs run none, allowlisted or not. - A reviewed exception takes an explicit
--ignore-scripts=falseat the call site.
- The allowlist only filters: it never re-enables scripts that
npm version floor
Installs fail when the active npm is older than the engines floor: the oldest version that supports the controls above.
- Enforcement:
enginesinpackage.jsonsets the floor.engine-strictin.npmrcmakes it fail closed.
- Floor policy:
- The floor rises as npm fixes enforcement gaps in the controls.
- It follows npm versions bundled with Node LTS releases, so a default toolchain passes the check.
- Netlify:
- Netlify’s Node-bundled default npm may be older than the floor;
NPM_VERSIONinnetlify.tomlpins one that satisfies it. - Bump the pin at least when the floor rises.
- Netlify’s Node-bundled default npm may be older than the floor;
Inert Netlify auto-install
Netlify’s automatic install at the start of a build is
neutralized by NPM_FLAGS in netlify.toml:
--dry-run: npm resolves and logs what an install would change, but writes nothing.--ignore-scripts: lifecycle scripts stay disabled by explicit instruction, not as a side effect of the dry run.
Scope: NPM_FLAGS is a Netlify build setting, not npm config; it applies
only to the automatic install, never to the build command’s npm runs.
Defense in depth: the real install is npm ci, which
replaces node_modules wholesale, so auto-install or build-cache residue there
does not survive into the build even though node_modules is invisible to the
clean-working-tree checks (they see only Git-visible changes).