# LLM Security — Version history Per-release notes for v7.0.0 onward. Imported from `CLAUDE.md` via `@docs/version-history.md`. ## v7.8.2 — Silent-failure fixes from the completion review (HIGH) Security patch covering five defects found in the v7.8.1 completion review. No feature changes; full suite green at 1901/0 (1865 + 36 new regression tests). The common thread in four of the five is **silent failure**: the scanner or hook reported success while the check it is named for did not run. That is the worst failure mode for a security tool, because a clean report is indistinguishable from a clean target. - **`hooks/scripts/pre-bash-destructive.mjs`** — the rule named "Filesystem root destruction (rm -rf /)" did not block `rm -rf /`. Its target alternation `(?:\/|~|\$HOME)\b` ended in a word-boundary assertion, which cannot hold after `/` or `~` at end-of-command. Measured: `rm -rf /`, `rm -rf ~`, `rm -rf /*`, `rm -fr /` and `sudo rm -rf /` all fell through to WARN — advisory only, exit 0, command executed. `rm -rf $HOME` and `rm -rf /usr` were blocked, which is why the gap survived: the rule looked functional from either end. The `\b` now sits on the `$HOME` alternative alone, so `$HOMEDIR` is still not swallowed. The prior test file had encoded the defect as expected behaviour, with a NOTE attributing it to merged `-rf` flags — a misdiagnosis, since `rm -rf /etc` blocks fine with merged flags. - **`scanners/entropy-scanner.mjs`** — the "test fixtures intentionally contain example secrets" suppression matched against the **absolute** path. Any ancestor directory name containing `test`, `spec`, `fixture` or `mock` therefore suppressed every entropy finding in the whole target: a repository cloned into a CI workspace, or checked out beneath a folder named `testing`, reported zero secrets with status `ok`. The rule now keys off the path relative to the scan root, which was already computed and passed alongside it. Genuine fixture suppression (a `*.test.mjs` file, a relative `tests/` directory) is unchanged. - **`scanners/ide-extension-scanner.mjs`** — `parseVSCodeExtension` signals failure as bare `null`; `parseIntelliJPlugin` signals it as a **truthy** `{ manifest: null, warnings }`. Only the former was guarded, so all five JetBrains failure paths (no `lib/`, `lib/` not a directory, `lib/` unreadable, no jars, no extractable jar) reached `manifest.hasSignature` and threw. `mapConcurrent` awaited without a per-item catch, so `Promise.all` rejected and the entire ide-scan aborted — one malformed plugin directory took down the scan of every other installed extension, including, in an audit context, a plugin that is malformed precisely because it is hostile. - **`scanners/content-extractor.mjs`** — this is the remote-scan indirection layer: agents are supposed to see a sanitized evidence package, never raw hostile content. `stripInjection` scanned the raw text *and* its decoded form, but removed matches with `sanitized.replace(match[0], …)` against the raw text only. A decoded-only `match[0]` does not occur in the raw text by construction, so the replace silently did nothing: the payload was handed to the agent verbatim through `sanitized_content` while the report announced a critical injection. Removal now redacts the offending source line, since decoding is not length-preserving and decoded offsets cannot be mapped back. A payload split across several lines still cannot be attributed; those findings carry `unstripped: true` instead of failing quietly. Whole-file redaction was rejected — `normalizeForScan` base64-decodes any long blob, so a benign asset could blank a file's entire evidence. - **`scanners/lib/ide-extension-parser.mjs`** — `decodeEntities` guarded `parseInt` with `Number.isFinite`, which bounds nothing, so a character reference above `0x10FFFF` raised `RangeError` in `String.fromCodePoint`. Contrary to how this was filed, the documented no-throw contract held: the per-field `safe()` wrapper catches it. The real effect was quieter — the field was replaced with `''`, so a `` carrying one such reference parsed as an empty name and name-based checks (JetBrains typosquat detection) ran against nothing. Severity is below HIGH: a document with a code point above `0x10FFFF` is not well-formed XML, so IntelliJ would reject the plugin as well. Undecodable references are now left literal. ## v7.8.1 — Auto-cleaner command-injection fix (CRITICAL) Security patch for a CRITICAL defect introduced with the v7.8.0 auto-cleaner hardening pass. No feature changes; full suite green at 1865/0. `scanners/auto-cleaner.mjs` validated candidate `.mjs`/`.js`/`.cjs` content by shelling out to ``node --check "${tmpPath}"`` via `execSync`. `tmpPath` is derived from the finding's `file` field — an untrusted scanned-repo filename. The F-2 containment guard that landed in v7.8.0 verifies the resolved path stays inside the scanned tree, but performs no shell-metacharacter handling, so a filename whose `"` terminates the interpolated quote injects a second command. Because `/security clean` runs in live mode by default, scanning a hostile repository was sufficient to execute attacker-chosen commands locally. The defect was reproduced with a live proof-of-concept before being fixed. Both subprocess sites now use `spawnSync` with an argv array — the syntax check and the CLI's inline scan-orchestrator fallback — so no shell interprets a path. As defense-in-depth, `applyFixes()` additionally refuses any finding whose `file` carries shell or control metacharacters, surfacing it as `skipped`. Regression coverage is deliberately split across both layers (`tests/scanners/auto-cleaner-rce.test.mjs`): one test drives `validateContent` directly to prove the shell is gone, because the metacharacter guard would otherwise stop the hostile input before it reached the sink and the test would pass without testing the fix. ## v7.8.0 — Trigger, signature, and AST-taint scanners Three new deterministic deep-scan scanners (TRG/SIG/AST), each with its own finding prefix, OWASP/AST mapping, policy block, fixtures, and graceful-skip behaviour. They target the skills/agents activation and code surface that the permission and shape-based scanners do not cover. Built behind a security-fix gate — the F-1/F-2/F-3 shell-injection and path-traversal fixes landed first. No existing scanner, hook, or command behaviour changes; full suite green (1863 tests, 0 fail). - **TRG — trigger/activation-abuse** (`scanners/trigger-scanner.mjs`). Inspects command/agent/skill `name` + `description` frontmatter for three activation-surface abuses: `TRG-shadow` (name collides with a built-in command/tool and intercepts it), `TRG-baiting` (description uses maximally-activating phrases — "anything", "always", "all files" — to bait indiscriminate invocation), and `TRG-broad` (a generic name plus a universal-applicability claim, so the unit auto-activates beyond its stated purpose). Skills/agents auto-activate on their description, so this is a skill-native surface not covered by the permission or taint scanners. Descriptions pass through the decode pipeline (zero-width strip → homoglyph fold → `normalizeForScan`) first, so obfuscated baiting still trips. Thresholds and the phrase/built-in lists are overridable via `.llm-security/policy.json` (`trg`). OWASP LLM06 (excessive agency); AST04. - **SIG — known-bad-identity signatures** (`scanners/signature-scanner.mjs`). A pure-Node signature engine for known-malware *identity* — webshells, reverse shells, cryptominers, hacktools — complementary to the shape-based entropy/taint scanners. Unlike a raw byte-matcher (e.g. YARA), every signature is tested against both the raw bytes and the project decode pipeline (base64/hex/url/entity/unicode decode, homoglyph fold, rot13), so obfuscated known-malware a byte-matcher misses is still caught. Rules ship in `knowledge/signatures.json`; families are policy-selectable. OWASP LLM03 (supply chain) primary; LLM02. - **AST — Python taint analysis** (`scanners/ast-taint-scanner.mjs`). Shells out to a PARSE-ONLY `python3` helper (`scanners/lib/py-ast-taint.py`) for variable-level, scope-aware taint analysis of Python skill code — higher recall/precision than the ~70%-recall regex `taint-tracer.mjs`. The helper only `ast.parse`s the target and never executes it, so analysing hostile code is side-effect-free. Falls back to the regex tracer whenever `python3` is unavailable, so the scan never hard-fails. OWASP LLM01, LLM02; AST02. Packaging/wiring: `TRG`/`SIG`/`AST` registered as valid finding prefixes (`scanners/lib/output.mjs`); all three wired into the scan orchestrator and policy loader; `knowledge/` added to the `package.json` `files` whitelist. ## v7.7.2 — Language consistency pass Norwegian had crept into surface text across v7.5–v7.7. Per the `~/.claude/CLAUDE.md` convention (English for code and documentation, Norwegian for dialog only), this release translates: the HTML Report-step in all 18 skill commands, the canonical CLI renderer `scripts/lib/report-renderers.mjs` (display strings + JS comments), the playground UI strings, the `skill-scanner-agent` and `mcp-scanner-agent` system prompts, the playground architecture prose + Recent versions table in the plugin `README.md`, the v7.7.x highlights in the plugin `CLAUDE.md`, the llm-security entries in the marketplace root `README.md` + `CLAUDE.md`, and six table cells in `docs/scanner-reference.md`. Demo-state fixture content for the `dft-komplett-demo` project (intentional Norwegian persona) and regex alternations that match Norwegian-language report markdown (`/^high|^høy/`, `/resolution|løsning/`) were preserved. CHANGELOG and this version-history file were deferred per operator decision — they remain in the language they were written in. No scanner, hook, or behavior changes — purely surface text. ## v7.0.0 — Severity-dominated risk scoring (v2 model, BREAKING) Three changes target the false-positive cascade on real codebases (hyperframes.com gave `BLOCK / Extreme / 100`, ~70% noise): 1. **Risk-score v2 formula** (`scanners/lib/severity.mjs`) — severity-dominated, log-scaled within tier. Replaces v1 sum-and-cap that collapsed every non-trivial scan to 100/Extreme. Tiers: critical → 70–95, high only → 40–65, medium only → 15–35, low only → 1–11. Verdict cutoffs realigned to new bands (BLOCK ≥65, WARNING ≥15). `info` findings are observability-only — counted in OWASP aggregates but contribute zero to risk_score, verdict, and riskBand (B3, v7.2.0 — was undocumented pre-7.2.0). See `severity.mjs` JSDoc for full contract. 2. **Rule-based entropy scanner with file-extension skip, 8 line-level suppression rules, and configurable policy** — extensions skipped (`.glsl/.frag/.vert/.shader/.wgsl/.css/.scss/.sass/.less/.svg/.min.*/.map`); line-suppression rules (GLSL keywords, CSS-in-JS, inline SVG, ffmpeg `filter_complex`, User-Agent strings, SQL DDL, `throw new Error(\`...\`)`, markdown image URLs). Configurable via `.llm-security/policy.json` `entropy` section (thresholds, `suppress_extensions`, `suppress_line_patterns`, `suppress_paths`). Envelope `calibration` block reports skip counters + effective thresholds + policy source. 3. **DEP typosquat allowlist expansion** — 22 npm + 5 PyPI entries for short-name tools that tripped Levenshtein detection on every modern codebase (`knip`, `oxlint`, `tsx`, `nx`, `rimraf`, `uv`, `ruff`, etc.). See `docs/security-hardening-guide.md` §6 for the calibration story. ## v7.1.1 — Scan-rapport narrative coherence (patch) Three coordinated edits address the whiplash symptom that survived v7.0.0 (numbers fixed, narrative still walked findings back as "false positive" in prose): (a) `agents/skill-scanner-agent.md` Step 2.5 mandates context-first severity assignment — every signal has exactly one disposition (suppressed OR reported), no per-finding walk-back; (b) `templates/unified-report.md` gains a `### Narrative Audit` block in Executive Summary surfacing `summary.narrative_audit.suppressed_findings.{count, by_category}` from the agent's trailing JSON; (c) both files updated from stale v1 risk-formula constants to the v2 model that has been authoritative in `severity.mjs` since v7.0.0. Counter is distinct from the existing top-level `output.suppressed` (`.llm-security-ignore` rule integer). Out-of-scope but flagged: `commands/scan.md:113-114` retains the v1 formula; resolution deferred to Batch B. ## v7.3.0 — MCP cumulative-drift baseline (Wave C of Batch C) Closes E14 from `docs/critical-review-2026-04-20.md`. The `mcp-description-cache.mjs` schema gains a sticky `baseline` slot per tool plus a 10-event rolling `history` array (FIFO). Cumulative drift = `levenshtein(current, baseline) / max(|current|, |baseline|)`; when the ratio crosses `mcp.cumulative_drift_threshold` (default 0.25), `post-mcp-verify.mjs` emits a separate MEDIUM `mcp-cumulative-drift` advisory. The existing per-update >10% drift signal is unchanged — both fire independently. Slow-burn rug-pulls that keep each update under the per-update threshold but cumulatively diverge from baseline are now caught. Baseline survives the 7-day TTL purge so detection persists across the full window. New `/security mcp-baseline-reset` slash command (plus `scanners/mcp-baseline-reset.mjs` CLI: `--list`, `--target `, or no-args clear-all) lets the user acknowledge a legitimate MCP server upgrade — clearing the baseline causes the next call to seed a fresh one from the incoming description; description, firstSeen, lastSeen, and history are preserved for audit. `LLM_SECURITY_MCP_CACHE_FILE` env var overrides the cache path for end-to-end testing without polluting the user's real `~/.cache/llm-security/mcp-descriptions.json`. ## v7.3.0 — Env-var deprecation warnings (D3 of Batch C, Wave D) Closes 8.7 from `.claude/projects/2026-04-29-batch-c-scope-finalize/plan.md`. `scanners/lib/policy-loader.mjs` exports a new helper `getPolicyValueWithEnvWarn(section, key, envVarName, defaultValue)` — env still wins per Preferences (existing behaviour), but when both the env-var AND the `policy.json` key are explicitly set, the helper emits a single per-process stderr line: `[llm-security] Deprecation: env-var ${ENVVAR} will be removed in v8.0.0; policy.json key ${section}.${key} also set — env wins for now. Suppress with LLM_SECURITY_DEPRECATION_QUIET=1.` Module-scoped `Set` dedupes per env-var name across call-sites. Four overlapping vars are wired through the helper: `LLM_SECURITY_INJECTION_MODE` ↔ `injection.mode` (in `pre-prompt-inject-scan.mjs`), `LLM_SECURITY_TRIFECTA_MODE` ↔ `trifecta.mode` and `LLM_SECURITY_ESCALATION_WINDOW` ↔ `trifecta.escalation_window` (in `post-session-guard.mjs`), `LLM_SECURITY_AUDIT_LOG` ↔ `audit.log_path` (in `scanners/lib/audit-trail.mjs`). `DEFAULT_POLICY` gains `trifecta.escalation_window: 5` to close the gap noted in the plan revisions table (M10). Env-only vars without policy.json equivalents (`LLM_SECURITY_UPDATE_CHECK`, `LLM_SECURITY_PRECOMPACT_MODE`, `LLM_SECURITY_PRECOMPACT_MAX_BYTES`, `LLM_SECURITY_IDE_ROOTS`, `LLM_SECURITY_MCP_CACHE_FILE`) are unchanged — they emit no deprecation signal because there is nothing to deprecate yet. ## v7.5.0 — Playground (additive surface, no scanner/hook behavior changes) Single-file SPA at `playground/llm-security-playground.html` (~10 200 lines) for onboarding, demo og workshop-bruk uten Claude Code-installasjon. Parser + renderer for alle 18 `produces_report=true`-kommandoer i `CATALOG`. State i IndexedDB primær (`llm-security-playground-v1`) med localStorage-fallback, sirkelfri Proxy + EventTarget store, microtask-batchet render. Theme-bootstrap med FOUC-prevention. 4 overflater: onboarding (5 grupper) → home (3 tracks) → catalog (20 kommandoer) ⇄ project (rapporter / oversikt / kontekst / eksport). Demo-state har tre prosjekter inline; `dft-komplett-demo` har alle 18 rapporter ferdig parsed for klikk-gjennom. Vendor-synket design-system under `playground/vendor/playground-design-system/` (sjekksum-låst via `MANIFEST.json`, redigeres aldri direkte). Test-fixtures under `playground/test-fixtures/` (én markdown-fil per kommando) er kontrakt-anker for parser-utvikling. Skjermdumper i `playground/screenshots/v7.5.0/`. Eksponerte vinduer-globaler for testing/automasjon: `__store`, `__navigate`, `__loadDemoState`, `__scheduleRender`, `__PARSERS`, `__RENDERERS`, `__CATALOG`, `__inferVerdict`, `__inferKeyStats`, `__renderPageShell`, `__handlePasteImport`. Inkluderer fix av `normalizeVerdictText` regex-rekkefølge: GO-WITH-CONDITIONS sjekkes før GO så betinget verdict ikke kollapser til ALLOW. ## v7.6.0 — Playground Tier 3-referanse-case (additive surface, no scanner/hook behavior changes) Playgroundet er nå en visuelt og strukturelt fullført referanse-implementasjon for `shared/playground-design-system/` Tier 3-supplementet. 8 nye Tier 3-komponenter integrert i de 18 rapport-rendererne: `tfa-flow` + `tfa-leg` + `tfa-arrow` (lethal trifecta-kjede med `