Methodology
Every score on this site is produced by a deterministic formula: the same findings always produce the same score. This page documents that formula exactly, every constant, threshold, and deduction, so you can recompute any number you see here and check our work. If a score looks wrong, the formula (or a scanner) is wrong, and we want to know.
Two audiences, two places to start: how scoring works if you are deciding whether to install a package, or for publishers if you ship one and want to know why it scored the way it did.
No findings does not mean no risk. Nerlo aggregates automated scanner output; automated scanning is one layer of defence, not a substitute for review.
An aggregator, not an oracle
We run a suite of scanners over each package and show you every verdict separately. We are a transparency aggregator, not a trust authority: no scanner is privileged, none can veto another, and the composite is a subordinate summary layered on top, not the headline. Each scanner produces its own scorecard with its own score, findings, and tool version, and those per-scanner reports are the primary view. The composite trust score is a convenience for readers who want one number; it never hides, overrides, or reranks a per-scanner verdict.
Eight of the scanners in the suite are off-the-shelf tools that we pin and run as-is:
- agentshield, agent-audit-kit, and cisco-skill-scanner: agent- and skill-focused analyzers.
- trivy: dependency vulnerability scanning. Trivy runs a second time, listed as trivy (image), against the container image a package ships when it ships one.
- osv-scanner: known vulnerabilities in dependencies via the OSV database.
- capslock and govulncheck: Go capability analysis and symbol-level vulnerability reachability, for artifacts that ship Go source.
Three are our own. We say so plainly here and label them Nerlo-original everywhere the suite is listed, because you should never have to guess which verdicts came from a third party and which came from us:
- nerlo-behavioral and nerlo-install-instruction: a behavioral ruleset and an install-instruction analyzer that we wrote and maintain, the same detectors behind the surface-divergence signal described below.
- nerlo-multi-source: the provenance diff that acquires a package from every distribution surface and reports behaviour present in what you install but absent from the source it claims to build from.
Provenance is a label, not a rank. A finding from a scanner we wrote deducts exactly what the same finding from an off-the-shelf tool would, and neither can overrule the other. Because each scanner is shown on equal footing, a clean report from one tool never launders a critical finding from another. When you disagree with a scanner, deselect it and the composite recomputes from only the tools you trust, described under “Your view” below.
Every distribution surface, scanned
A single MCP server rarely ships from a single place. The same package can reach you as git source, a GitHub release asset, an npm tarball, or a pip wheel, and those copies do not have to be identical. We acquire every distribution surface we can find for a package and scan each one independently, rather than trusting that the code on the repository's main branch is the code that actually gets installed.
- git source: a shallow clone of the repository, checked out but never built or executed.
- GitHub release assets: the tarballs and archives attached to a published release, fetched and unpacked as inert files.
- npm tarball: obtained with
npm pack --ignore-scripts, so the package is downloaded and expanded but its install/postinstall lifecycle scripts are never run. - pip wheel: wheels only, which are pre-built archives that are unpacked, not compiled from a source distribution that could execute arbitrary
setup.pycode.
Acquisition happens inside a sandbox that never executes untrusted package code. Nothing is installed, built, or run while a surface is fetched. Each package is reduced to a tree of files that the static scanners then read without ever invoking it.
Nothing is executed, including by us
Every phase that runs on this registry reads files. We never execute the package we are scanning, at any point in a scan. The scan container is started with sleep as its own command, the scanner binaries are run inside it against a copy of the package mounted read-only, and the package's own entry point is never invoked. The single place we run a package manager at all is lockfile resolution, which uses npm install --package-lock-only --ignore-scripts precisely so that no install hook fires.
We state that as a limit rather than a feature, because the design called for more. The pipeline has four phases. The two that run are static: static analysis, and the dependency and supply-chain audit. The two that would have executed the package are held back, and you should hear which from us rather than infer it from a scorecard.
- Injection fuzzing would send adversarial input to a running MCP server. We do not own those endpoints, and a company whose product is a security badge sending unsolicited probe traffic to other people's infrastructure is a liability with nothing authorizing it. Building a mock from the declared schema and probing that instead fails for a different reason: it scores the mock, not the package.
- Deep behavioral audit would execute the package and watch what it reached for, against decoy credentials and decoy endpoints. We measured the containment it depends on, Firejail nested inside the scan container, and found it failing open: it exited
0while the process it was meant to confine still opened a network connection. A containment control that reports success without having run is worse than no control, because nothing downstream can tell the two apart. So we did not ship the phase.
Both stay reserved until containment can be measured rather than assumed, and you do not have to take that on faith. The weights actually applied to a scan are published with its results, and the fuzzing and deep-behavioral terms read 0.0 on every scan in this registry. If that day never comes, they stay at zero and this page keeps saying so.
Surface divergence: the source-vs-published diff
Once we hold more than one surface for the same package, we diff them against each other. The signal we care about most is behavior that is present in a published or release package but absent from the source it claims to be built from: an npm tarball or a release asset that contains network calls, credential reads, or obfuscated logic that never appears in the git tree.
This is exactly the shape of the postmark-mcp and FakeGit supply-chain-divergence incidents, where the repository looked clean but the shipped package did not. A methodology that only scans the source repository is blind to that class of attack by construction. Scanning every surface and flagging the divergence is how we catch it: the divergence itself is a finding, surfaced in the report rather than averaged away.
How scoring works
Scoring runs in two layers. Each scanner scores the package it just read, and then those scores combine into one composite. Both layers use the same badge ladder, and both are arithmetic over findings. No model sits in between. The constants are ours and so are the weights; we publish the reasoning for each one below so you can argue with the choice rather than take the number on faith.
One badge ladder
Per-scanner badges and the composite badge use the same thresholds, so there is a single ladder to learn. It is not the whole rule for the composite badge, though: two floors sit on top of it, described immediately below the table.
| Badge | Score range | API value |
|---|---|---|
| Clean | 85 – 100 | Verified |
| Caution | 60 – 84.99 | Caution |
| Flagged | below 60 | Unsafe |
The third column is the value the API returns and the value ?badge= takes, which is unchanged. We renamed only what you read. “Unsafe” was a claim the evidence did not support: it means “at least one scanner scored this below 60”, which on our own corpus was true of packages scoring 95 out of 100 with no critical finding anywhere. “Verified” failed in the other direction, because we do not verify anything — we run tools and publish what they found. “Caution” is unchanged; it was already advice to a reader rather than a verdict on someone’s work.
The composite badge is not the composite score
This is the part of our methodology most likely to surprise you, so it gets its own section rather than a footnote. If you take the composite score off a package page and read it against the ladder above, you will sometimes get a greener badge than the one we publish. That is not a bug and it is not a stale number. Two rules floor the composite badge after the score is computed, and both can only ever make the badge worse than its score alone would suggest. Neither ever makes it better.
- The worst scanner sets the floor. The composite badge is never greener than the worst badge any scanner that completed handed down. One scanner at Flagged means the composite reads Flagged, whatever the composite score is. Scanners that returned no verdict, because they did not apply to this package or did not finish, are excluded: there is no verdict to floor with.
- Accumulated risk caps at Caution. A composite that is otherwise Clean is capped at Caution once the findings across all completed scanners add up, counting each high as 2 and each medium as 1, to 6 or more. Risk spread thinly across several scanners still warrants a warning, even when no single scanner dropped below Clean.
Why we do this. The composite is an average, and averages dilute. Without the floor, a majority of clean scanners could arithmetically outvote the one scanner that found something serious, and the badge would read Clean while a scorecard one scroll down said otherwise. We would rather the summary be too pessimistic than quietly launder a finding. The cost of that choice is the surprise this section exists to remove.
A worked example, from a real shape in our registry. A package is scanned. Every scanner that applied but one returns Clean, most of them at 100. The one dissenter returns 58, two points under the Caution cutoff, on a pair of high findings and nothing worse. No scanner anywhere reports a critical. The composite score, a weighted mean, comes out at 96.33, which sits well inside the Clean band. The published badge is Flagged, because the worst completed scanner said Flagged and the floor carries that verdict up. Both numbers are correct and we publish both. The score tells you what the suite concluded on average; the badge tells you the worst thing any one tool found.
The consequence worth stating plainly: a package badged Flagged has not necessarily scored below 60. Most of the ones in our registry have not. Read the badge as “at least one scanner raised something,” then open the per-scanner grid to see which scanner, what it found, and whether you agree with it. That grid is the primary view for exactly this reason.
What a badge does not tell you
A badge summarises what a fixed set of automated tools reported about one version of one package at one moment. It is not a certification, an endorsement, or a judgement about the people who wrote the code. Being specific about the edges:
- Flagged is not a finding of harm. It means at least one scanner scored below 60, or a scan stopped early. It does not mean we found malware, and it does not mean the package does anything malicious. Where we do have evidence of that, it is named in the scorecard.
- Clean is not a guarantee. It means nothing crossed our thresholds. Scanners miss things, and a scanner that found nothing has told you what it did not find, not that there is nothing to find.
- Most packages here never asked to be rated. We scan publicly available code and report what publicly available scanners returned, with the evidence attached. A badge is a summary of tool output, not a verdict we reached about someone's work.
- It goes stale. Every badge is stamped with the scan that produced it. Code published after that scan has not been looked at.
- It says nothing about quality, licensing, or fitness. We measure a narrow set of security signals. A well-built package and a badly-built one can carry the same badge.
No findings does not mean no risk. Nerlo aggregates automated scanner output; automated scanning is one layer of defence, not a substitute for review.
If you publish one of these packages and think a badge misrepresents it, here is what you can do about it.
Per-scanner scores
A scanner's score starts at 100 and loses points for each finding, by severity. The result is clamped to 0–100.
| Finding severity | Deduction | Note |
|---|---|---|
| Critical | −40 | also caps the score at 59 (Flagged) even if the arithmetic lands higher |
| High | −15 | also caps the score at 84 (Caution): a high finding can never hold Clean |
| Medium | −5 | one medium alone stays Clean (95) |
| Low | −2 | |
| Informational | −0.5 |
- Critical cap: any critical finding forces the score to at most 59: a scanner that finds a critical issue can never award Clean or Caution, no matter how clean the rest of the report is.
- Native scores: when the underlying tool ships its own 0–100 score (AgentShield does), we use the tool's number instead of the deduction table, then apply the critical cap on top. The scanner's scorecard always shows which tool and version produced it.
- No findings = 100. A perfect score means that scanner found nothing. It does not mean the package is safe. That is why every scanner's report is shown side by side.
The composite score
The composite is a weighted mean over the components we could actually assess. Each scan phase's component score is the average of the scanners that completed in that phase; the weights below are renormalized to sum to 1.0 across only the phases that produced a result. A phase that has not been built yet, or that no scanner found applicable, is left out of the average entirely. It is never counted as a zero, so a clean partial scan is not dragged down by work we haven't done.
The weights are choices, not measurements. Each row says why we picked it, and the exact weights applied to a given scan are published with its results. The fuzzing, deep-behavioral, and vitality weights have never been applied to anything. The first two are the dynamic phases described under nothing is executed; the third needs inputs nothing collects yet. They are marked below, and marked rather than deleted, because the constants are real and the renormalization above is what removes them. What that leaves is narrow and we would rather you heard it here: every composite in this registry is drawn from static analysis and dependencies alone — 0.5 each where both phases completed, and static analysis at 1.0 where the dependency phase produced nothing. Where neither completed there is no composite to publish.
| Component | Default weight |
|---|---|
| Static analysis (SAST)Reads every file we acquired, so it is the broadest signal we have. It gets a full quarter. | 0.25 |
| Dependencies / supply chainMost compromises arrive through something the package pulls in rather than through code its publisher wrote. We weight it equal to the code itself. | 0.25 |
| Fuzzing / injection resistanceNever appliedReserved. This is the phase that would send adversarial input to a running server, and we do not run servers, so it has never contributed to a score on this registry. | 0.250.0 applied |
| Deep behavioral auditNever appliedReserved. This is the phase that would execute the package and watch it. We could not prove the containment it needs, so we did not ship it, and it has never contributed to a score on this registry. | 0.150.0 applied |
| Maintenance vitalityNever appliedReserved. The sub-scores below are implemented and tested, but nothing in the scan pipeline collects the commit, issue, and dependency-age inputs they need, so no scan has ever supplied a vitality value. | 0.100.0 applied |
- Permission adjustment: when a package declares permissions beyond the baseline (read-only filesystem, outbound network, credential store), the weight of the corresponding risk category increases by 0.10 per extra permission, capped at 0.40 for any single component, with the increase redistributed from the others. The exact weights used for a scan are published with its results.
- Behavioral deductions, which have never fired: the formula carries a term that subtracts points directly from the weighted sum when a package interacts with a decoy credential or endpoint (20 points per critical trigger, 5 per non-critical). Those decoys only exist while a package is running, and we do not run packages, so this term has been zero on every scan we have ever published. We describe it because it is in the engine and you will see it in the API response, not because it has ever moved a number.
- Degraded assessments fail closed. If a scanner that was expected to run instead errored or timed out, the assessment is marked degraded and the composite badge is capped at Caution: it can never read Clean. A scanner that could not finish is treated as unknown risk, not zero risk.
- The composite is one view among many. It never overrides or hides a per-scanner verdict. Read the scorecards.
- The badge is floored by the worst scanner. The weighted mean above produces the composite score. The composite badge is that score put through the ladder and then floored, so it can never be greener than the worst completed scanner's verdict. See the composite badge is not the composite score.
A worked example
Every number below comes from the two tables above. Nothing else is needed to reproduce it, and you can run the same arithmetic against any scorecard we publish.
Step 1: one scanner. A static-analysis scanner reports one high, two medium, and three low findings. Starting from 100: −15 for the high, −5 twice for the mediums, −2 three times for the lows. That is 100 − 15 − 10 − 6 = 69. The high finding also caps the score at 84, which does not bite here because 69 is already below it. 69 sits in Caution.
Step 2: the component scores. Say that scanner was the only one to complete in its phase, so the static-analysis component is 69. Dependencies came out at 90. Those are the two phases that run, and they are the two components this scan produced. Fuzzing, deep behavioral, and vitality produced nothing, so all three are dropped from the average rather than scored zero.
Step 3: renormalize, then combine. The two surviving default weights (0.25 and 0.25) sum to 0.50, so each is divided by 0.50 to bring the total back to 1.0:
| Component | Score | Weight | Contribution |
|---|---|---|---|
| Static analysis (SAST) | 69 | 0.5000 | 34.50 |
| Dependencies | 90 | 0.5000 | 45.00 |
| Composite | 79.50 | ||
79.50 reads as Caution, and the badge floor agrees: the one scanner that completed came out at 69, which is itself Caution, so the composite badge could not have read Clean regardless of the arithmetic. This is the shape of a real scan here whenever both phases complete: two components, renormalized over 0.50, and no behavioral deduction, because there is no phase that could produce one.
Maintenance vitality
This component is specified and implemented, and it has never run. Nothing in the scan pipeline collects the commit history, issue response times, or dependency ages it needs, so no scan has supplied a vitality value and the term is renormalized away exactly like the two dynamic phases. We document it because the arithmetic is real and the field is in the API response you can read; we are not claiming it contributed to any score you have seen. When it does run, it will work as follows.
The vitality component measures whether a project is actively maintained, over the trailing 90 days. It is the mean of three sub-scores, each linearly interpolated and clamped to 0–100:
- Commit activity: 100 at ≥ 4 commits/month, 0 at none.
- Issue responsiveness: 100 when the mean response time is ≤ 48 hours, 0 beyond 30 days.
- Dependency freshness: 100 when dependencies are within 30 days of their patch releases, 0 when any is more than 180 days stale.
Where a signal simply doesn't exist (no issues ever filed, no dependencies), that sub-score gets full marks: absence of data is not treated as evidence of neglect.
“Your view”: the scanner filter
When you deselect scanners on a package page, the composite is recomputed from only the scanners you kept, each with equal weight (vitality joins as one additional equally-weighted term when there is a vitality value, which today there never is). Phase weights are deliberately dropped: the filter expresses “these are the tools I trust,” not “these are the phases I care about.” The behavioral-deduction term described above is not filterable either, but that is a property of the formula rather than something you will observe: it has never been non-zero, because nothing executes a package and therefore nothing can trip a decoy.
For publishers
If you ship one of these packages and you think the score is wrong, the remedy is the scan itself, not a conversation with us.
- Fix, then rescan. Scores are recomputed from scratch on every scan. Nothing is cached and no penalty carries over, so if the finding was real, landing the fix is the whole remedy: the next scan reflects it.
- Check the finding against the evidence. Every verdict on a package page names the scanner that produced it and links the finding it rests on. The scoring engine is a pure function of those inputs, so a verdict you disagree with is a disagreement with a named tool's output on a named commit — something you can reproduce and, where the tool is wrong, take up with the tool.
- We do not edit results on request. There is no appeal or dispute process, and this is deliberate rather than an omission. An aggregator that moved a score because the package's author asked would not be worth reading; what makes the number trustworthy is that nobody — including us — can talk it into changing. It moves when the code moves.
- An override would be disclosed, never folded in. Moving a badge by hand is technically possible — the record and the operator console exist — but there is no longer any way to ask for it: the publisher-facing appeal endpoints were withdrawn on 2026-08-16 and now answer
410 Gone. If an override is ever applied it carries the operator's identity, their written justification, and the badge before and after, kept for 24 months alongside the scan it changed, and the signed report says the registry is publishing a badge the scan did not produce rather than quietly amending what the scan found.
Check our work
Every scan stores its per-scanner reports, component scores, and the exact weights applied, immutably for 24 months. The scoring engine is a pure function: same inputs, same score. If you recompute a number from this page and get a different answer than we show, see our Trust page for how to verify the scan pipeline itself, and tell us, publicly.