How to Run the Patch Simulator
Preview the impact of a candidate upgrade — which CVEs clear, how many transitive dependencies unblock, what other packages might need to move with it — before you open a PR.
What the Simulator Does
A patch simulator answers: “if I bump package X from version A to version B, what changes?” The answer is three numbers:
| Output | Meaning |
|---|---|
cleared_cves[] | CVEs in the current closure of X that are not present in the closure of X@B |
unblocked_transitive_count | Transitive dependencies currently pinned at vulnerable versions because the new range was incompatible with X@A — bumping X to B lets them move |
related_pins[] | Packages whose constraints will need to change to accommodate the upgrade (peer-dep conflicts, version-range overlaps) |
The simulator does not run npm install or pip install — it walks the registry’s dependency metadata directly, so it’s fast (sub-second for most upgrades) and side-effect-free.
Prerequisites
- Inventory populated.
- A target package and version in mind (or, ideally, picked off the Patch Priority leaderboard).
Step 1: Open the Simulator
Three entry points:
- Click any row on the Patch Priority leaderboard.
- Open Reports → Patch Simulator and search for the package by name.
- Hit the API:
POST /api/patches/simulate.

Step 2: Run a Simulation from the UI
- Package: e.g.
lodash. - Current version:
4.17.20(defaults to your most-installed version if blank). - Target version:
4.17.21(defaults to the leaderboard’s recommended upgrade if invoked from there). - Click Simulate.
The result page shows:
- Cleared CVEs: a table with CVE ID, severity, KEV/EPSS flags, and the path through the dependency tree where the CVE was reachable.
- Unblocked transitives: list of dependencies that can move once X is upgraded, with their current → unblocked-target versions.
- Related pins: any peer-dep / version-range conflicts the upgrade would create. This is the column to read carefully — if
related_pinsis empty, the upgrade is safe to apply directly. If it contains entries, those entries will need PRs of their own.
Step 3: Run a Simulation from the API
For automation:
curl -X POST https://chain305.com/chainproxy/api/patches/simulate \
-H "Authorization: Bearer $CHAINSAW_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"package": "lodash",
"ecosystem": "npm",
"from_version": "4.17.20",
"to_version": "4.17.21",
"scope": {
"client_id": "ci-frontend"
}
}'
The scope object is optional — without it, the simulation runs across your entire inventory. With it, the simulation is restricted to the closures of packages installed by the named client.
Response shape:
{
"cleared_cves": [
{"id": "CVE-2026-2345", "severity": "CRITICAL", "kev": true, "epss": 0.74,
"paths": ["frontend-build > webpack > lodash"]}
],
"unblocked_transitive_count": 3,
"unblocked_transitives": [
{"package": "webpack", "from": "5.88.0", "to": "5.92.1"},
...
],
"related_pins": [],
"score_delta": 12.4
}
Step 4: Compare Multiple Targets
The most useful question is rarely “what does upgrading to X do?” — it’s “which of these N candidate targets is best?” Pass targets[] instead of to_version:
curl -X POST https://chain305.com/chainproxy/api/patches/simulate \
-H "Authorization: Bearer $CHAINSAW_TOKEN" \
-d '{
"package": "lodash",
"ecosystem": "npm",
"from_version": "4.17.20",
"targets": ["4.17.21", "4.17.22", "5.0.0"]
}'
Response is keyed by target version:
{
"results": {
"4.17.21": {"cleared_cves": [...], "unblocked_transitive_count": 0, ...},
"4.17.22": {"cleared_cves": [...], "unblocked_transitive_count": 1, ...},
"5.0.0": {"cleared_cves": [...], "unblocked_transitive_count": 14, "related_pins": [12 entries]}
}
}
The shape makes the trade-off explicit: 5.0.0 clears more CVEs and unblocks more transitives but requires 12 dependent PRs.
Step 5: Use the Simulator in CI
The simulator is fast enough to run on every Renovate / Dependabot PR. The pattern:
- PR opens with a dependency bump.
- CI step calls
/api/patches/simulatewithfrom_versionfrom the lockfile andto_versionfrom the PR. - CI annotates the PR with the simulation result:
cleared_cves: [...],unblocked_transitive_count: N. - If
related_pinsis non-empty, CI adds aneeds-coordinated-mergelabel and links the dependent PRs.
# .github/workflows/chainsaw-simulate.yml
name: Chainsaw simulate
on:
pull_request:
paths: ['package.json', 'package-lock.json', 'requirements.txt', 'Pipfile.lock']
jobs:
simulate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# pr-scan diffs the PR against its merge base. The default
# shallow clone does not contain the base commit, so the scan
# would silently find nothing.
fetch-depth: 0
- uses: Chain305/chainsaw-core/enforcement/github-actions@action-v1
with:
mode: warn
comment-on-pr: "true"
env:
CHAINSAW_TOKEN: ${{ secrets.CHAINSAW_TOKEN }}
CHAINSAW_SERVER: ${{ secrets.CHAINSAW_SERVER }}
The Action runs chainsaw pr-scan over the PR’s changed manifests and posts
a single summary comment (comment-on-pr is an Action input, not a CLI flag).
To drive the patch simulator itself, call /api/patches/simulate directly as
shown in Step 3 — chainsaw pr-scan has no --simulate flag. Its full flag
set is --base, --head, --repo-path, --strict, --json and
--output-file.
Step 6: When the Simulator Disagrees with Your Intuition
A common case: you bump a package to fix CVE-X, the simulator reports cleared_cves: []. That means CVE-X is not reachable through the path you’re upgrading. Likely causes:
- The CVE was disclosed in a different version range than you thought — confirm against the GHSA / CVE record’s affected versions.
- Your inventory has multiple installed versions of the same package — the upgrade only clears the CVE for the path you’re bumping, not other paths. Check Inventory → by-package for the same package and look at the version distribution.
- The CVE applies to a sub-dependency, not the package you’re bumping. The simulator surfaces this in
unblocked_transitivesrather thancleared_cves.
Verification
- From the leaderboard, click a row → simulator opens with the package preselected → impact numbers match what the leaderboard advertised.
- Run a simulation for a known-no-op upgrade (e.g.,
1.0.0→1.0.0) → response is empty cleared list, zero transitives unblocked. - The CI annotation pattern in Step 5 posts a comment on a test PR.
Related Topics
- How to Use the Patch Priority Leaderboard — the ranking layer that decides what to simulate first.
- How to Use Billy MCP Tools — Billy can simulate on your behalf via the
chainsaw_simulate_patchMCP tool. - How to Manage Repositories and Upstream Mirrors — the registry metadata the simulator reads.