# Changelog
All notable changes to the LLM Security Plugin are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [8.0.0] - 2026-09-18
Major release. The breaking part is small and was announced in v7.3.0: four
deprecated `LLM_SECURITY_*` env-vars and the long-dead `riskScoreV1()` are
removed. The larger part is structural and behaviour-preserving: the detection
tables (injection lexicon, codepoint carriers, OWASP map, secret shapes, malware
signatures) are now built from a vendored, versioned data repository under
`scanners/commons/` instead of source literals, each swap proven content-identical
against a golden baseline. Several recall and correctness fixes ride along.
2253 tests, 2247 pass, 6 skipped, 0 fail.
### Added
- **`llms.txt` at the repository root.** A minimal entry point for an AI agent
already standing in this repository: H1, one blockquote saying what the
artifact does, the install block quoted verbatim from `README.md`, and link
lists to the docs. It cites `README.md` rather than restating it, so the two
cannot drift into two different install instructions. Deliberately minimal:
llms.txt is a convention, not a ratified standard, so a format change stays
cheap.
- **A stated deprecation notice period** in `SECURITY.md`. The practice already
existed — the four `LLM_SECURITY_*` env-vars were announced in v7.3.0 and are
removed in v8.0.0, five minor releases later — but nothing said it was the
rule, so an adopter could read the changelog and still not know what to expect
next time. The promise: announcement in the changelog, at least one minor
release **and** no fewer than 30 days before removal, removals only in a major,
one stated exception for security-critical removal, and an explicit note that
this is a notice period and not an SLA.
- **Commons conformance, spec 1.1.** The suite runs the vendored commons
conformance corpus through the real entry points and publishes a
schema-shaped declaration of the result. The set of commons tables this
runtime implements is declared from the same constant the runner uses to
accept a scope, so the declaration cannot drift from what actually ran. Cases
scoped to a table this runtime does not implement are reported as
*not-applicable* — enumerated by name, counted as skipped rather than passed,
and kept in the denominator. An empty declaration fails the suite instead of
turning every case not-applicable and passing with zero cases run. The
declaration artifact is generated per run and gitignored.
### Removed — BREAKING
- **The four `LLM_SECURITY_*` configuration env-vars deprecated in v7.3.0.**
`.llm-security/policy.json` is now the only source. A removed variable is
**inert**: it neither warns nor configures, so a project relying on one
silently returns to the default. See the Migration section in `README.md`
for the detection commands.
| Removed env-var | Policy key | Default |
|-----------------|------------|---------|
| `LLM_SECURITY_INJECTION_MODE` | `injection.mode` | `block` |
| `LLM_SECURITY_TRIFECTA_MODE` | `trifecta.mode` | `warn` |
| `LLM_SECURITY_ESCALATION_WINDOW` | `trifecta.escalation_window` | `5` |
| `LLM_SECURITY_AUDIT_LOG` | `audit.log_path` | unset |
| `LLM_SECURITY_DEPRECATION_QUIET` | *(none)* | dies with the warning it silenced |
Env-vars with no policy equivalent are unaffected:
`LLM_SECURITY_PRECOMPACT_MODE`, `LLM_SECURITY_PRECOMPACT_MAX_BYTES`,
`LLM_SECURITY_UPDATE_CHECK`, `LLM_SECURITY_MCP_CACHE_FILE`,
`LLM_SECURITY_IDE_ROOTS`.
`getPolicyValueWithEnvWarn` and its one-shot stderr deprecation line are
deleted from `scanners/lib/policy-loader.mjs`; the four call sites collapse
to `getPolicyValue`. Hook strings that advertised a removed variable as the
escape hatch now name the policy key — a blocked user following the old text
would have set a variable that does nothing.
- **`riskScoreV1()`** in `scanners/lib/severity.mjs`, plus its
`SEVERITY_WEIGHTS_V1` table. `@deprecated` since v7.0.0, kept for
diff/comparison, with zero callers in code or tests. The v1 weights are
recorded here for anyone re-deriving an old score:
`critical 25, high 10, medium 4, low 1, info 0`, summed and capped at 100.
`riskScore()` (v2) is untouched, so no score, band, or verdict moves.
- **Two documented env-vars that were never implemented.**
`LLM_SECURITY_SCR_OFFLINE` (`docs/ci-cd-guide.md`) and `LLM_SECURITY_OFFLINE`
(`examples/supply-chain-attack/README.md`) were promised as OSV.dev / npm-audit
kill-switches; no code has ever read either. The docs now say plainly that
there is no kill-switch and that an air-gapped run must block egress at the
network layer. The `LLM_SECURITY_AUDIT_*` wildcard phrasing (README,
`docs/scanner-reference.md`, `docs/security-hardening-guide.md`) is narrowed
to the single real key.
### Changed
- **Posture category 12 (Rule of Two) no longer keys off the identifier
`TRIFECTA_MODE`.** The check was `/TRIFECTA_MODE/i` over the session-guard
source, which measured what a constant was *named* rather than whether
enforcement was configurable at all. It now matches
`getPolicyValue('trifecta', 'mode', …)`, and still accepts a pre-v8 vendored
guard reading `LLM_SECURITY_TRIFECTA_MODE` — third-party projects carry their
own hook copy and are equally configurable. Without this, every correctly
migrated project would have dropped from PASS to PARTIAL. The PARTIAL
finding now recommends the policy key instead of the removed env-var.
- **Detection tables are built from vendored commons data, not source
literals.** `scanners/commons/` is a pull-only subtree of the
`llm-security-commons` data repository, inside the published `scanners/`
directory so it ships with the plugin. Five tables moved, each measured
position-by-position against the literal it replaced before the swap, with the
golden dump diffed record-for-record afterwards: the codepoint carriers
(zero-width, bidi, Unicode Tag range, homoglyph map), `OWASP_MAP`, the 83
injection patterns, the 19 fixed credential shapes in `pre-edit-secrets.mjs`,
and the 7 SIG malware signatures. Published module surfaces are unchanged —
the tables are re-exported from where they always lived.
`knowledge/signatures.json` is **removed** rather than kept as a second,
ungated copy of the SIG table; operator rules via `sig.custom_rules_path` are
unaffected. The commons location is resolved from the plugin's own directory
only — never from a scanned target's `policy.json`, which would let a hostile
repository substitute an empty detection corpus. Loaded artifacts are
deep-frozen. An unresolvable injection lexicon or SIG ruleset writes one line
to stderr naming the disabled capability instead of scanning with zero
patterns and reporting clean; it never throws, because a module-load throw in
a hook would break the tool call rather than degrade the scan.
- **Golden gate `coverage` block relabelled to what it measures.** It probes
payload strings against patterns in-process, which is static reachability, not
what the reference run exercised. `coverage.kind = 'static-reachability'`;
`patternsExercised` → `patternsReachable`, `uncoveredPatterns` →
`unreachablePatterns`, `tablesExercised` → `corpusContains`. The gate pins the
label so it cannot be dropped quietly.
### Fixed
- **``, so `