Loop: CVE Audit
Drive the current repository's dependencies to a defensible security state, one verified fix at a time. This is a loop skill: it runs in iterations with explicit terminal states, and it is designed to be re-run on a schedule (new CVEs appear over time — this loop never "finishes" permanently).
Loop contract
The loop ends only in one of these states. Every run must report which one it reached.
| Terminal state | Meaning |
|---|---|
| CLEAN | No exploitable high or critical CVE remains. |
| ASSESSED | High/critical findings remain, but every one has evidence-backed reachability analysis and a user-approved risk decision. |
| BLOCKED | A precondition failed (no dependency graph, no advisory access, broken build baseline). Reported, nothing changed. |
Hard rules for the whole loop:
- Never hide findings. No adding entries to audit ignore files,
osv-scanner.tomlexclusions,--excludeflags, severity threshold changes, or deleting a dependency's usage just to silence a finding. The only two ways a finding leaves the list are: a verified fix, or a user-approved risk decision. - "Not reachable" is a claim that requires evidence (format below). It is never the default.
- New CVEs introduced by an upgrade count as findings and go back into the queue. Verify the scan after every change, not just at the end.
- Preserve unrelated work. Never commit, revert, or stash changes that existed before the loop started.
Phase 0 — Preconditions (fail fast)
Before touching anything, verify you can actually do the job:
- The repo has a resolvable dependency graph (lockfile or manifest
present:
package-lock.json/pnpm-lock.yaml/yarn.lock,poetry.lock/requirements.txt,Cargo.lock,go.mod, etc.). - An advisory-backed scanner runs successfully (see Phase 1).
- The baseline is green: build and tests pass before any change. If they don't, you cannot attribute regressions to your fixes.
If any of these fail → terminal state BLOCKED. Report exactly what is missing and stop. Do not scan from memory; do not guess CVEs.
Phase 1 — Baseline scan
Use the ecosystem-native tool, falling back to osv-scanner (covers
all ecosystems via OSV.dev):
| Ecosystem | Primary tool |
|---|---|
| npm/pnpm/yarn | npm audit --json (or pnpm audit --json) |
| Python | pip-audit (or uv pip audit) |
| Rust | cargo audit |
| Go | govulncheck ./... |
| Any / multi | osv-scanner --lockfile <path> |
Record in the baseline (this anchors the whole run — the goalposts must not move mid-loop):
- Tool name and version, advisory source, and scan timestamp.
- Full list of high/critical findings: CVE/GHSA id, package, installed version, direct or transitive, fixed-in version if any.
Low/medium findings are noted in the final report but are not loop targets — chasing them is how this loop loses focus.
Phase 2 — Reachability triage
For each high/critical finding, determine whether the vulnerable code is actually reachable in this project. This is the highest-judgment step and also the loop's cheat vector — declaring things unreachable is the lazy path to termination, so the evidence bar is strict.
A reachability assessment must contain all of:
- Vulnerable symbol(s): which function/class/behavior the advisory actually concerns (read the advisory; the package name is not enough).
- Import path: where this project (or a dependency on the path to
it) imports the affected module — cite
file:line, or show the transitive chain (A → B → vulnerable-pkg). - Verdict with proof:
- Reachable: a concrete call path from project code to the vulnerable symbol, or the exploit precondition shown to exist (e.g. the vulnerable parser is fed request data).
- Not reachable: why the call path cannot exist — the vulnerable symbol is never invoked (show the search), the feature is disabled, the dependency is dev/build-only and never ships, or the exploit precondition is structurally absent.
Prose like "this doesn't appear to be used" without a cited search or
call chain does not qualify and must be treated as reachable by
default. govulncheck does call-path analysis natively for Go — prefer
its verdict there.
Phase 3 — Ranking
Rank the reachable findings by, in order: severity, reachability confidence, exposure (does it face untrusted input / production?), and availability of a remediation (patched version exists, upgrade distance). Work strictly from the top of this list.
Phase 4 — Remediate (one finding per iteration)
For the highest-ranked finding, apply the smallest credible change:
- Prefer, in order: patch-level bump of the affected package → minor bump → lockfile-level override/resolution to force a fixed transitive version → direct-dependency upgrade that pulls the fix → replacing the dependency (ask first).
- One finding per iteration. Do not batch unrelated upgrades — when verification fails you must know which change caused it.
- Two-attempt cap: if two remediation attempts for the same finding fail verification, stop trying. Revert to the last green state and route the finding to the risk-decision path (Phase 6) with the failed attempts documented.
Phase 5 — Verify
After every change:
- Install/lockfile update completes cleanly.
- Build passes. Tests pass. (Same commands as the Phase 0 baseline.)
- Re-run the scan with the pinned baseline tool and version: the finding is gone and no new high/critical finding appeared.
Keep the change only if all three hold; otherwise revert and count the attempt. Then return to Phase 3 with the updated finding list.
Phase 6 — Approval gates
Stop and ask the user (do not proceed on your own) before:
- Major-version or otherwise breaking upgrades.
- Replacing or removing a dependency.
- Anything that changes production config or deployment.
- Accepting risk on any remaining finding. Present the reachability evidence and a recommendation; the user makes the risk decision.
Final report
Every run ends with, regardless of terminal state:
- Terminal state reached (CLEAN / ASSESSED / BLOCKED).
- Inventory: every high/critical finding from the baseline and its outcome (fixed in iteration N / risk-accepted / blocked), plus the low/medium list for awareness.
- Reachability evidence for every finding assessed not-reachable or risk-accepted, in the Phase 2 format.
- Fixes applied: package, old → new version, and the verification results (build/test/scan output summary) proving each.
- Remaining risks and recommended follow-ups.
- Scanner name, version, and scan timestamps (baseline and final).
Claims without fresh command output are not proof. Show the re-scan.
Running it on a schedule
This loop is a natural recurring job — new advisories are published daily against unchanged code. Good triggers:
- Weekly (e.g.
/loopor a cron/scheduled session invoking this skill). - After any change to a lockfile or dependency manifest.
On a scheduled re-run, diff against the previous run's report: only new findings (or findings whose risk decision has expired) create work. Previously approved risk decisions stay approved unless the advisory's severity or the project's exposure changed — re-litigate those, not everything.