Security Guides
osv-scanner in Practice — pnpm, Ignoring CVEs, Offline Mode, and the Errors You Hit
The second-day questions: pnpm-lock.yaml support, ignoring one CVE via osv-scanner.toml, the three offline flags and the error they cause, exit codes 0/1/127/128 in CI, and the flags renamed from v1 to v2.
Who this is for: people who have already installed osv-scanner and hit friction. This covers the second-day questions, not the install. Everything here follows the official documentation.
Pointing it at a lockfile
# a single lockfile
osv-scanner scan source -L pnpm-lock.yaml
# walk a directory
osv-scanner scan source -r ./my-project/When the format cannot be inferred from the filename, prefix the path with the parser to use:
# parse an oddly named file as requirements.txt
osv-scanner scan source -L 'requirements.txt:/path/to/extra-requirements.txt'
# path containing a colon — lead with a bare ':' to keep filename inference
osv-scanner scan source -L ':/path/to/my:projects/package-lock.json'Why the lockfile and not the manifest
A manifest (package.json and friends) records the range you asked for, not what actually got installed. A lockfile carries resolved, exact versions, so the scan matches what is really in the tree. Judging a range and hoping is far weaker evidence.
How to check your own setup
When results look wrong, go through these five checks before adding more flags. Each one only inspects your own project.
Check the version
osv-scanner --version. v2, released in 2025, changed the command shape and several flag names, so instructions written for v1 often fail as written. The official documentation now targets v2.Check what it read
--format json --all-packages (--all-packages only works with JSON output).Check the exit code
echo $? in bash or $LASTEXITCODE in PowerShell. The table below gives the meanings.Check the config actually applied
osv-scanner.toml applies only to files in the directory where it sits; it does not propagate to child directories. In a monorepo scanned with -r, put one next to each lockfile, or pass a single file to everything with --config.Check where the offline database lives and how old it is
OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY if set, otherwise the user cache directory, then the system temp directory. Inside, files follow osv-scanner/<ecosystem>/all.zip. If their modification dates are old, so is your verdict.Exit codes and how CI should treat them
| Exit code | Official meaning | In CI |
|---|---|---|
0 | Packages found, no known vulnerabilities matched | Pass |
1 | Packages found, vulnerabilities matched | Fail (a finding: fix it, or ignore it with an expiry) |
127 | General error | Fail (the scan itself broke) |
128 | No packages found (nothing readable was picked up) | Fail (fix the path or add the lockfile) |
The documentation reserves 1–126 for result-related outcomes and 129–255 for errors unrelated to results. Splitting CI handling three ways — 0 passes, 1 is a finding, anything else is a broken scan — makes failures much faster to diagnose.
Ignoring a specific vulnerability
Drop an osv-scanner.toml into the directory being scanned:
[[IgnoredVulns]]
id = "GHSA-xxxx-xxxx-xxxx"
ignoreUntil = 2026-12-31
reason = "Feature is not used and the code path is unreachable; re-evaluate at the December inventory"id(required): the vulnerability identifierignoreUntil(optional): after this date it is reported againreason(optional): write it anyway — for the next person, who is usually you
--config=/path/to/config.toml overrides the per-directory files and applies everywhere. Note that ignoring a vulnerability also ignores the ones treated as its aliases.
Give every ignore an expiry date instead of removing it for good
An entry without ignoreUntil is an item that vanishes permanently and that nobody revisits. Our rule is: every ignore carries an expiry and a reason — when the date passes it comes back, so the deferred decision cannot be quietly forgotten. Open-ended ignores added to silence CI are exactly what turns into an incident six months later.
Offline mode: three flags, three jobs
This is where most confusion lives.
| Flag | What it does |
|---|---|
--offline | Fully offline. Judges against a previously downloaded local database, does not update it, and sends no project or dependency information anywhere |
--offline-vulnerabilities | Only the vulnerability matching is offline; other work (such as transitive resolution) may still use the network |
--download-offline-databases | Permits fetching/updating the local database — only effective when an offline flag is also set |
--offline
fully offline; no database update, nothing sent out
--offline-vulnerabilities
only the matching is local; other work may use the network
--download-offline-databases (inert alone)
needs one of the above → on its own you get "databases can only be downloaded when running in offline mode"
The errors and what causes them
databases can only be downloaded when running in offline mode→--download-offline-databaseswas passed on its own; it needs an offline flag alongside it--offlinewith no local database yet → you get an error saying the database is missing; the first run has to fetch itno package sources found→ nothing at that path was recognised as a lockfile or manifest; name it with-Lor walk with-r
Choosing without guessing
- Air-gapped run →
--offline(plus--download-offline-databaseson the first run) - Only the vulnerability data must stay local →
--offline-vulnerabilities - Pin where the database lives → the
OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORYenvironment variable - Debugging → run with no flags first, then narrow
When flags from a v1 article do not work
Many search results were written for v1. The main changes listed in the official migration guide:
| v1 | v2 |
|---|---|
--experimental-offline | --offline |
--experimental-download-offline-databases | --download-offline-databases |
--experimental-call-analysis | --call-analysis |
--experimental-licenses and related | merged into --licenses |
--docker / -D (containers) | osv-scanner scan image <image> |
--json | --format=json |
--skip-git | --include-git-root (meaning reversed) |
osv-scanner <dir> still works as a shortcut for osv-scanner scan source <dir>. Writing scan source out in scripts makes the intent clear to the next reader.
Common mistakes and the fix
Common mistakes
- CI treats "any exit code except 1" as success
|| trueappended to make failures go away- One
osv-scanner.tomlat the repository root, assumed to cover subdirectories - Only
package.jsoncommitted, with no lockfile - Assuming
--offlinestill resolves the transitive dependencies of apom.xml - Ignores written without
ignoreUntil
The fix
- Pass on 0 only, so 128 (nothing read) cannot go green
- Silence individual findings with expiring ignores instead
- Place a file in each directory, or apply one everywhere with
--config - Commit the lockfile (for JavaScript:
package-lock.json,pnpm-lock.yaml,yarn.lockorbun.lock) - Transitive resolution is disabled offline; if the network is allowed, use
--offline-vulnerabilities - Always add a date and a reason, and decide again when the date arrives
A Maven pom.xml does not list every resolved version the way a lockfile does. By default osv-scanner computes the transitive graph using the deps.dev resolver (or Maven Central with --data-source=native). That needs the network and is skipped under --offline and --no-resolve. In a fully air-gapped run, expect the results to cover only the dependencies written directly in the pom.xml.
A real example: why Log4Shell was hard to answer
When Log4Shell (CVE-2021-44228) surfaced in December 2021, many organisations could not say quickly whether they were affected. They had never added Log4j themselves; it came in through frameworks and other libraries. CISA's guidance asked organisations to enumerate installed software comprehensively and compare it against the list of affected software.
Applied to osv-scanner, that means checking two things:
- Transitive dependencies are actually being read: lockfiles where the ecosystem has them, and for Maven a run with resolution enabled (not offline, not
--no-resolve) - The inventory really contains the libraries that arrive indirectly — print it with
--format json --all-packagesand look
Whether you can answer "are we affected?" within minutes on disclosure day depends on getting that inventory right beforehand.
How it differs from the neighbours
| Tool | Data it reads | What it is good at |
|---|---|---|
| osv-scanner | OSV.dev | Across ecosystems, per lockfile, can run offline |
npm audit / pnpm audit | npm advisory database | Immediate for npm projects, nothing extra to install |
| Dependabot | GitHub Advisory | Opens update PRs — proposes the fix, not just the finding |
| Trivy | Several (incl. OSV) | Container images and OS packages as well as app dependencies |
Choosing exactly one is the mistake. Different databases surface different things. We run pnpm audit as the daily automated check and use osv-scanner for cross-lockfile checks.
This site's view: a scanner will not tell you your pin went stale
One problem only shows up in real operation. When you pin a transitive dependency to an exact version with overrides, that package stops receiving patch updates — and a scanner only speaks up when a known vulnerability exists. So "the version you froze is now old" is a state nobody reports. We ran into this three times before adding our own daily check that compares every pin against the newest patch in its major line. A scanner shows known vulnerabilities today; conditions likely to leave you exposed later need a different mechanism.
Sources
- OSV-Scanner documentation — Usage / Offline Mode / Configuration
- Supported Artifacts and Manifests
- OSV-Scanner documentation — Output (formats and exit codes) / Migration Guide (v1 to v2)
- CISA — Apache Log4j Vulnerability Guidance
Read next
- Getting started: installing and using osv-scanner
- Prioritisation: deciding what to patch first (CVSS, EPSS, KEV)
- Incident: Log4Shell (when transitive dependencies hid what was in use)
- Practice: the practice of vulnerability response / defending against npm supply-chain worms
FAQ
QDoes osv-scanner support pnpm-lock.yaml?
Yes. npm, yarn and pnpm lockfiles are all supported. Point at one directly with `osv-scanner scan source -L pnpm-lock.yaml`, or walk a tree with `-r`.
QHow do I ignore a single CVE?
Put an `osv-scanner.toml` in the scanned directory and add an `[[IgnoredVulns]]` entry with the `id`. You can also set `ignoreUntil` (an expiry date) and `reason`, so an exception records when and why rather than disappearing forever. Passing `--config=/path/to/config.toml` overrides the per-directory files and applies globally.
QWhy do I get 'databases can only be downloaded when running in offline mode'?
Because `--download-offline-databases` only takes effect when an offline flag is also set — it is not usable on its own. Combine it with the offline flag when you want the local database fetched as part of the run.
QIf I already have npm audit and Dependabot, do I need osv-scanner?
They read different databases and cover different ground. npm audit uses the npm advisory database and is npm-only; Dependabot mainly proposes updates on GitHub; osv-scanner reads OSV.dev across ecosystems (npm, PyPI, Go, Maven and more), works per lockfile, and can run fully offline. One catching something the other misses is normal, so treat them as different jobs rather than picking one.
QHow should CI treat osv-scanner exit codes?
Per the official docs: 0 means packages were found and none matched a known vulnerability, 1 means vulnerabilities were found, 127 is a general error, and 128 means no packages were found at all. Treat only 0 as success. A rule like 'anything but 1 passes' turns 128 — nothing was scanned — into a green check.
QWhy don't --experimental-offline or --docker from older articles work?
They were renamed or moved in v2. The official migration guide maps --experimental-offline to --offline, --experimental-download-offline-databases to --download-offline-databases, --docker to the `osv-scanner scan image` command, and --json to `--format=json`. Run `osv-scanner --version` first to see which major version you have.