Every number this tool prints comes from one file, src/analysis/assess.ts.
Everything upstream reports facts; everything downstream renders. This page
documents the arithmetic so that a verdict you disagree with is a verdict you
can argue with.
signals ──▶ category ──▶ risk 0–100 ──▶ severity ──▶ recommendation
Signals are found first and are independent of category. The category is then decided from the signals plus the script text. Only then is risk computed — and the category is an input to that, because context changes what a signal is worth.
A signal is one observed behaviour, with the matched evidence attached. A detector answers a narrow mechanical question and never decides whether the answer is bad.
| Signal | Weight | What it observes |
|---|---|---|
pipe-to-shell |
45 | a download piped straight into a shell |
credential-path |
35 | a reference to a file that normally holds credentials |
dynamic-eval |
30 | eval, new Function, vm.runIn* |
obfuscated-source |
28 | source that cannot be reviewed by eye |
base64-blob |
25 | a large embedded base64 payload |
shell-profile-write |
25 | a shell profile or scheduler — how install-time code persists |
process-spawn-detached |
18 | a process that outlives the install |
network |
15 | any network access |
home-escape |
12 | reaching outside the package into the user home |
shell-out |
10 | starting another process |
env-read |
6 | reading environment variables |
native-toolchain |
0 | node-gyp, prebuild-install, cmake-js |
git-hook-install |
0 | husky, simple-git-hooks, lefthook |
The scale is calibrated so that any single signal above 30 is on its own enough to demand a human read the script, and so that the ordinary shape of a legitimate build lands in the low tens.
The last two are worth zero deliberately. They describe what a script is, not whether it is dangerous, and they earn their keep by driving classification rather than by moving the number.
Each detector contributes at most one signal per package. A script that calls
fetch eleven times is not eleven times more interesting than one that calls it
once.
A signal means different things in different scripts. Reaching the network is
unremarkable in prebuild-install, whose entire job is downloading a prebuilt
binary; it is a strong signal in a script whose purpose is unclear.
| Category | Discounted signals |
|---|---|
native-build |
network ×0.2, shell-out ×0.2, env-read ×0.3 |
binary-download |
network ×0.6, shell-out ×0.5 |
codegen |
shell-out ×0.3 |
dev-tooling |
shell-out ×0.4, home-escape ×0.5 |
Discounts are multipliers, not suppressions. The signal is still reported with its evidence, and it still moves the score — a native build that reaches the network is marginally more interesting than one that does not, just not alarmingly so.
Some pairs mean more together than apart.
| Combination | Bonus | Why |
|---|---|---|
network + credential-path |
+25 | touches credential files and contacts the network |
network + env-read |
+20 | the shape of every registry credential-exfiltration incident |
dynamic-eval + base64-blob |
+20 | decodes an embedded payload and evaluates it |
network + shell-out |
+10 | downloads and then executes |
A bonus is suppressed when either of its signals is heavily discounted in that
category. prebuild-install reading npm_config_* to pick a mirror is not an
exfiltration pattern, and scoring it as one would teach people to ignore the
finding that matters.
Two categories carry risk before any signal is counted.
| Category | Base | Why |
|---|---|---|
binary-download |
25 | the bytes that execute are not the bytes the lockfile hashed, and it breaks in an offline build for reasons that look nothing like the cause |
unknown |
10 | by definition, nobody has attributed a purpose to it |
Without these, a tidily written downloader scored info while being recommended
for review — a contradiction a reader is right to distrust.
risk is capped at 100 and mapped to a band:
| Risk | Severity |
|---|---|
| ≥ 70 | critical |
| ≥ 45 | high |
| ≥ 25 | medium |
| ≥ 10 | low |
| < 10 | info |
One override: a script classified unknown that also reaches the network is
never below medium, however the arithmetic lands.
| Category | Recommendation |
|---|---|
native-build, codegen |
allow |
dev-tooling, telemetry, notice, no-op |
deny |
binary-download, unknown |
review |
With one rule above all of them: nothing at high or critical is ever
recommended for automatic approval, whatever its category. A node-gyp build
that also pipes a download into a shell is not an ordinary native build.
scriptgate allowlist writes the allow and deny recommendations and leaves
review out.
A dev-only dependency is not discounted. It would be convenient and it would be wrong. Install scripts from dev dependencies run on developer laptops, which hold the npm tokens, the SSH keys and the cloud credentials that production build agents do not. The dev flag is reported as context so you can weigh deployment risk separately; it never moves the number.
Package popularity is not an input. Download counts measure how many people would be affected by a compromise, not how likely one is. Weighting by popularity would systematically under-rate exactly the packages an attacker would most want to compromise.
Publisher identity is not an input. It would require the network, and maintainer-account takeover — the Axios incident of March 2026, among others — is precisely the case where publisher reputation is worth nothing.