Version 0.1 · August 2026 · Forge Registry
Forge's trust score answers one question: how likely is this to be bad? Who published it, did they prove it, did a scan run, and what did the scan find.
Blast radius answers the other one: how bad if it is?
They are different questions, they take different evidence, and Forge reports them as two axes. This note says what raises a blast radius, what it is computed from, and — at least as importantly — what it deliberately refuses to claim.
Why this is not part of the trust score
The obvious product is one number: likelihood × impact, a single "adoption risk" figure, one sort key. Forge does not ship that, for four structural reasons.
1. The merged number means neither thing. A trust score of 82 decomposes into rows a publisher can act on — verify your domain, publish with provenance, fix the high-severity CVE. Fold impact in and 82 means "some mixture of how trustworthy you are and how powerful you are". Nobody can act on that, and nobody can explain it. Forge's commitment is that scores are earned and the data behind them is public; a number whose components cancel out honours neither half of that.
2. A high blast radius is not a defect. A filesystem MCP server is supposed to write files. A shell server is supposed to run commands. Charging them for it produces a registry where the most useful tools carry the worst grades, and where a publisher's only route to an A is to expose less capability — which is not a security improvement, it is a worse product.
3. The score would move for nobody's act. Every weight in the trust score
is something a publisher did or failed to do. Blast radius is a property of
what the software is. A server that adds a legitimate write_file tool in a
minor release would watch its trust grade drop with no change in its security
posture.
4. The two are read together, and better apart. The decision a person actually makes is two-dimensional:
| High trust | Low / unrated trust | |
|---|---|---|
| High blast radius | Fine — install it, and scope the token narrowly | Stop |
| Low blast radius | Fine | Probably fine; little to lose |
Multiplying those into one scalar destroys the only information that made the read useful.
This is the same argument Forge already applies to
eval results and to
endpoint health, and it is enforced the same way: the
trust scorer takes no blast-radius input, the blast-radius scorer reads no
trust input, and src/__tests__/blast-radius.test.ts pins both directions.
The bands
Blast radius is reported as a band, not as a precise figure. The underlying 0–100 number exists and is published, but the inputs behind it are inferences, and rendering an inference to two significant figures claims a precision it does not have.
| Band | Score | What it means |
|---|---|---|
| Contained | 0–17 | Nothing measured reaches past the entry itself — no execution, no credential worth stealing. |
| Moderate | 18–33 | The ordinary local-process reach: runs as you, holds a service credential, sees what you point it at. |
| Extensive | 34–59 | Reaches outside its own process in ways that are hard to undo — mutation, deletion, a data store, or an account-wide grant. |
| Critical | 60–100 | Arbitrary execution, or execution together with a live credential. A compromise is a compromise of the host or the account. |
| Unmeasured | — | The floor and the ceiling land in different bands. Read the range. |
The bands are calibrated against real cases rather than round numbers, and the weights below are calibrated against the bands rather than the other way round. Two cases pin the scale:
- A locally-installed server with a single execution-class tool scores 42
before anything else is known about it. That entry must not read "moderate"
while we are still waiting to learn what credentials it holds — which is why
extensivestarts at 34. - A locally-installed server with a shell tool and any secret credential
clears 60. The
criticalband is defined as "arbitrary execution, or execution together with a live credential", so if that entry did not land there the model would be contradicting its own definition. An earlier draft weighted execution at 28 and produced exactly that contradiction.
What raises it
Six dimensions, each capped independently so the published breakdown adds up to
the score with nothing quietly absorbed. The caps sum to exactly 100, and every
factor row is clamped to its dimension's remaining headroom as it is added — so
the rows a reader can see always sum to the score they are shown, with no
after-the-fact Math.min absorbing the difference.
| Dimension | Cap |
|---|---|
| Execution | 40 |
| Credentials | 18 |
| Locus | 12 |
| Bundle | 14 |
| Escalation | 10 |
| Supply | 6 |
Execution — up to 40
What the entry can make happen, read from the tool surface Forge extracted
(statically from the published artifact, or from a live tools/list handshake
against a hosted endpoint).
| Capability class | Weight | Example tool names |
|---|---|---|
| Runs commands | 34 | exec_command, run_shell, spawn_process, eval_code |
| Deletes data | 26 | delete_file, drop_table, truncate_log, kill_job |
| Mutates data | 20 | write_file, edit_document, rename_key, upload_blob, install_package |
| Reaches a data store | 15 | run_sql |
| Privileged, unclassified | 12 | flagged by the scanner, matching no class above |
| Read-only surface | 3 | a surface we read that contains none of the above |
Ordered by how hard the damage is to undo rather than by how alarming the verb sounds. Arbitrary execution tops it because it subsumes every other row — a shell is a delete tool, a write tool and a network client at once. Deletion outranks mutation because a mutation is usually recoverable from the thing that was mutated. A data-store verb sits below both because its reach is one store rather than the host.
A second privileged tool adds 1 point, capped at 6 in total: more tools widen the surface, they do not multiply it.
A "read-only surface" scores 3 rather than 0 on purpose. Reading is reach — whatever the entry can see is inside whatever its credentials open — and the score has to stay distinct from the 0 an entry we never measured receives.
Prompt entries (Agent Skills, subagents) confer no tools of their own. What they can do is name MCP servers they require, which is borrowed reach: 6 points, +3 per additional server, capped at 14.
Credentials — up to 18
Weighted by what the credential opens, not by how secret it is.
| Kind | Weight | Why |
|---|---|---|
| OAuth grant | 15 | Account-scoped: reaches everything that account can reach on that service, usually until someone revokes it. The credential a stolen copy is most useful to somebody else. |
| API key | 10 | One service, whatever the key was scoped to when it was minted — frequently more than this entry needs. |
| Environment variable | 4 | Configuration that gates the install without conferring access. |
Each additional distinct credential adds 2, capped at 3.
Locus — up to 12
Where the code runs, and therefore whose authority a compromise inherits.
| Weight | ||
|---|---|---|
| Runs on your machine | 8 | Started as a local process by your MCP client: your shell, your environment, your filesystem, your network. |
| Runs hosted | 6 | Your machine is not executing its code — but every prompt and every tool argument you send it leaves your machine, and what happens next is not observable from here. |
| Both | 12 | Ships an installable package and a hosted endpoint; whichever you pick, the other is available. |
| Runs as a prompt | 4 | Text loaded into a client that already held its own authority. |
Bundle — up to 14
A collection installs many separately-authored units on one command: 7 points, plus 1 per two bundled units. A collection's trust score covers its installer package, not its contents — none of the bundled units were individually verified by installing it, and each carries its own reach.
Supply — up to 6
Install-time code execution (5 points when lifecycle scripts are declared) and the size of the transitive tree behind it (1–3 points by count). Installing a package installs everything under it, each from a separate maintainer with the same install-time reach.
Escalation — up to 10
The one non-additive term in the model, and the one that needs a justification rather than a weight.
Execution and a secret are each other's exit. A tool that runs commands can read the credential out of the process environment and post it anywhere; a credential worth stealing gives the command somewhere worth going. Scored as two independent rows, the pair lands mid-table beside a server that merely holds a token — which understates the only combination in this model with a named, mechanical escalation path.
- Execution-class tool and a secret-bearing credential: +10
- Mutation- or deletion-class tool and a secret-bearing credential: +6 (a file it writes may be a file you later run — a write tool is an execution tool on a delay)
Plain environment variables never trigger it. A non-secret configuration value is not worth exfiltrating, and letting one fire this term would make it near universal and therefore meaningless.
What we do not know, and how it is reported
This is the part of the model that matters most, because the failure mode is one-sided. Reporting "contained" for something Forge simply never measured is the dangerous error. "We do not know what this can do" and "it cannot do much" are opposite claims.
So every result is a range:
score— the floor. What the evidence in hand already implies.ceiling— the floor plus everything the gaps could still be hiding.tier— the band, or"unknown"whenever the floor and the ceiling land in different bands.
An unscanned npm entry does not score 0 and read "contained". It scores 8 — what its package coordinates alone imply — and reads "contained to critical — not scanned yet, tool surface unknown", which is the true state of Forge's knowledge about it.
Four dimensions can go unmeasured:
| Gap | Recorded when | Headroom |
|---|---|---|
| Execution | No tool surface, and the reason is not-scanned or not-extractable |
40 |
| Credentials | No credential found from the publisher, the upstream registry, or the README | 18 |
| Locus | No package coordinates and no declared endpoint | 12 |
| Supply | Never scanned | 6 |
Execution gaps are reported with Forge's existing ToolSurfaceGap vocabulary —
not-scanned, not-extractable, none-detected, not-applicable — rather
than a second, parallel set of reasons. Two of those four are answers, not
gaps, and are scored as such:
none-detected— the artifact was read and no tool registration matched. That is an observation about the package.not-applicable— an Agent Skill is a prompt, not a tool server. It has no tool surface to be missing.
The ceiling is kept honest in the other direction too. An entry with an observed surface containing nothing privileged cannot grow an escalation term by having its credentials filled in, so its ceiling does not include one. Overstating the ceiling is as dishonest as understating the floor: it would turn every unscanned entry critical and teach readers to ignore the field.
Known weaknesses
Stated rather than hidden.
privilegedis inferred from tool names. A tool calledsummarizethat shells out is invisible to this model; one calledwrite_summarythat only formats a string is over-counted. The capability classes refine that inference — they are a partition of the scanner's own keyword list, so they cannot disagree with it — but they do not repair it.- A prompt's reach is its client's. This model scores what a skill or subagent confers (nothing of its own, plus any MCP servers it declares it needs), not what it could talk an agent into doing. That second question is what Forge's prompt-injection scanning is for, and the two are not substitutes.
- Credentials are conservatively inferred. An empty list means nobody declared one and no README showed one — which is why it is recorded as a gap rather than as "needs nothing".
- A tool surface is a point-in-time artifact. It is as old as the scan that read it. The observation timestamp travels with it everywhere Forge renders one.
Where it appears
- Entry profile — the panel under the trust score, with every factor and every gap spelled out.
GET /api/v1/packages/:id— asblast_radius, inside the Ed25519-signed body, so a proxy cannot soften "runs arbitrary commands and holds an OAuth grant" on its way to the caller.GET /api/v1/packages/:id/install-planand theforge_install_planMCP tool — carried on the plan, and pointedly not wired into its warnings or its verdict. Capability is not a finding.GET /api/v1/search?blast=…andGET /api/v1/registry?blast=…— a band filter.unknownselects exactly the entries Forge could not measure; every other value excludes them, on the same rule?health=follows.forge search --blastandforge verifyin the CLI.
There is deliberately no sort key. An ordinal built from inferred inputs, over a corpus where a large share of entries are honestly unmeasured, makes a bad ranking: sorting ascending would put the least-known listings at the top of a list that reads as "safest". The filter answers the same question without implying an order the data cannot support.
Reference implementation
src/lib/blast-radius.ts — pure, no I/O, no clock. computeBlastRadius() is
the whole model; the weight tables above are exported constants in that file and
this document is the prose view of them. The two must not drift.