Two BLOCKs are not the same claim
A byte-for-byte match to a published advisory and a package that was uploaded last Tuesday both stop the install. Rendering them identically hides the difference that decides whether it is worth ten seconds or ten minutes.
DepWall answers in three tiers: ALLOW, ASK, BLOCK. That is the whole contract and we like it — three answers is a thing a human and an agent can both act on without a manual.
It is also not enough, and we have been quietly aware of that for a while. The tier says what the gate would do. It says nothing about how sure the call is, and those are not the same claim.
The two BLOCKs#
Here are two real verdicts from our own engine.
BLOCK known-malicious
the tarball npm serves for [email protected] is byte-for-byte the one
reported malicious in MAL-2026-11524
BLOCK provenance + maturity
"some-name" was not found in the npm registry
very new package (12d old)
The first is not an inference. Somebody confirmed that tarball was malicious, published its digest, and the registry is serving those exact bytes. There is nothing left to interpret.
The second is a fact plus a circumstance. The name does not resolve, which is a real signal — but so is the possibility that somebody typed it wrong, or that it is an internal package and the registry allowlist is not configured yet.
Both print BLOCK. Rendering them identically is the interface telling you they are the same size of claim, and they are not.
What we added#
A confidence number and a band word on every ASK and BLOCK, plus one line naming what the call rests on. It ships in the browser extension first, because that is the surface where a human is reading rather than a process is exiting.
The number rates the evidence, not the package. It is not a probability that something is malicious, and it is not a risk score — nothing in the system could back either of those. It reports how directly the signals that fired observe what they claim.
| Signal | Strength | What it actually saw |
|---|---|---|
known-malicious | 99 | The bytes match an advisory's bytes |
build-scripts | 86 | Read the script that will run |
obfuscation-density | 84 | Measured on the shipped code |
doc-injection | 82 | The text addresses the agent, and is quoted |
hallucinated-name | 80 | In a documented feed, and thin in-registry |
slopsquat | 62 | Edit distance to a popular name |
install-scripts (warn) | 58 | A lifecycle script exists |
llm-judge | 55 | A model's reading of the code |
maturity | 42 | The package is new, or barely downloaded |
attestation | 36 | No signature is present |
Severity splits the signals whose meaning moves with it.
install-scripts at critical has read the script body and found
exfiltration patterns; at warn it means a lifecycle script exists, which is true
of a large share of entirely legitimate packages. Scoring those the same would be the
original problem in miniature.
Three decisions that keep it honest#
ALLOW gets no meter
This was the first thing we got wrong in the design, and the most dangerous. A confidence bar next to a clean result reads as a safety score — "94% safe" — and no signals firing is the absence of a measurement rather than one. So a clean verdict draws nothing, and says what it always said: no signals fired.
Corroboration counts kinds of check, not findings
"New package" and "unsigned" fire together constantly. They are not two independent confirmations; they are one observation wearing two hats, because nothing published last week has had time to be signed by anybody. Counting them separately would inflate a weak circumstantial case into a strong-looking number.
So the bonus applies per distinct class of evidence — the name, the code, the metadata, an exact match, a model's opinion — and it is capped. Enough weak signals can never add up to certainty, which is precisely the arithmetic a scoring system is prone to.
"Could not check" is not a weak detection
Some findings mean the check did not complete: a lockfile that would not parse, a dependency tree past the traversal limit. Those are real reasons to stop and bad reasons to draw a mostly-empty bar, because a mostly-empty bar reads as "we looked and found little". When everything that fired was a coverage gap, it says Not verified and fills nothing.
The colour, and a mistake we made twice#
We first drew the meter on its own red-to-green scale, the way a password strength meter works. Green for strong evidence, red for weak.
The result was a green bar sitting beside a red BLOCK badge — two answers in one card that look like they disagree, when they are answers to different questions. Worse in the other direction: a weak ASK drew a red bar and shouted louder than a strong BLOCK.
The meter now wears the verdict's own colour, and strength is how much of the bar is filled. Four segments of red beside a red BLOCK reads as "very sure this is bad". Two of amber beside an amber ASK reads as "worth a look, but we are guessing". Red keeps meaning one thing. Colour is never the only carrier either — the number and the band word are always printed, and the bar is a real ARIA meter with a spoken value.
What this does not fix#
A number next to a verdict is still a number, and numbers invite the reading that they are comparable across everything. 82 on a name heuristic and 82 on a script read are the same confidence in two very different kinds of finding, and the basis line under the bar exists because the number alone would flatten that.
It also does not change a single verdict. Nothing about the tiers moved; the gate blocks exactly what it blocked before. What changed is that when it stops you, it now tells you whether to spend ten seconds on it or ten minutes.