The category answers one question: if I deny this, what breaks?
That is the question npm approve-scripts leaves entirely to the reader, and it
is the reason allowlists get rubber-stamped. Faced with 60 packages and no
information, approving all of them is the only move that reliably keeps the
build working.
Rules are ordered, and the order encodes consequence rather than confidence. A package that both builds a native addon and prints a funding notice is a native build; calling it a notice and recommending denial would break the install. So the categories whose denial costs the most are tested first, and the harmless ones only match when nothing above them did.
Deny it and: the native addon is never built. require throws at runtime,
with a message about a missing .node file that points nowhere near the cause.
Matched by: a binding.gyp with no declared install script, or a
native-toolchain signal — node-gyp, node-pre-gyp, prebuild-install,
prebuildify, cmake-js, node-gyp-build.
Recommended: allow.
The implicit case matters more than it sounds. npm supplies node-gyp rebuild
for any package shipping a binding.gyp without its own install hook, and npm
v12 blocks that synthesised script exactly as if it had been written out. There
is nothing in package.json to grep for, so every search-based approach misses
it — and it is the category whose denial is most expensive.
Deny it and: the downloaded binary is missing. Commands that call it fail.
Matched by: a download piped into a shell, or network access together with evidence that an artefact is being fetched — a URL naming an archive or executable, a write of the response to disk, or the script saying outright that it is downloading a release.
Recommended: review. Base risk 25.
Two separate problems, both real. The security one: the bytes that execute are not the bytes your lockfile hashed, so lockfile integrity does not cover them. The operational one: it fails in an air-gapped or offline build, and the error looks nothing like its cause.
The distinction from an analytics beacon is load-bearing. Both open a socket; only one leaves you with a missing binary when denied, and telling a reader that denying an exfiltration script "will leave a missing binary" is worse than saying nothing.
Deny it and: generated files are absent. The package may fail to import.
Matched by: a build or generation toolchain — tsc, babel, rollup,
webpack, esbuild, swc, patch-package, graphql-codegen, protoc.
Recommended: allow.
Deny it and: git hooks are not installed. Nothing else changes, and CI is entirely unaffected.
Matched by: husky, simple-git-hooks, lefthook, core.hooksPath.
Recommended: deny.
Checked ahead of the network rules: a git-hook installer that fetches its own binary is still a git-hook installer, and denying it is still free.
Deny it and: no install analytics are sent. The package works normally.
Matched by: scarf, segment.io, mixpanel, amplitude, or the words
telemetry or analytics in the script text.
Recommended: deny.
Deny it and: a console message is not printed.
Matched by: funding and thank-you messages, opencollective, a script whose
whole body is echo or console.log — and only when no build toolchain and no
network access are also present.
Recommended: deny.
Deny it and: nothing changes.
Matched by: exit 0, true, :, a bare echo, or a package that declares
only hooks which are not install-time — a prepare on a registry dependency,
for instance.
Recommended: deny.
Deny it and: unknown. Read the script before deciding.
Matched by: anything the rules above did not claim — and, deliberately, by
any script that reads a file which normally holds credentials, whatever else it
appears to do. Base risk 10, and never below medium severity when the network
is involved.
Recommended: review, always.
This is the honest category, and it exists so the tool can decline to answer.
scriptgate allowlist leaves these out of the generated block rather than
guessing, and reports how many it withheld. An allowlist that silently approves
everything the analysis did not understand is worse than no allowlist, because
it looks like review happened.
There is no legitimate install-time reason to open ~/.npmrc or ~/.ssh, so a
script that does is never given a category that would imply a purpose — no
matter how much it also looks like a downloader.
prepare runs at install time only for dependencies resolved from a git URL,
which is the one case where a prepare script belongs to code that did not come
through the registry's publishing path. For a registry dependency it never runs
on install, and reporting it would be noise.
scriptgate makes this distinction from the lockfile's resolved field, so it
holds only for npm projects with a lockfile. Elsewhere prepare is treated as
not install-time, which is correct for the overwhelming majority of packages.