Skip to content
>_ITDITDWeb Security Platform

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.

Published 2026-09-04 Updated 2026-10-06 Last verified 2026-10-06 10 min read

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.

1

Check the version

Run 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.
2

Check what it read

The run log shows each file it read and how many packages it found there. Confirm the lockfiles you expected are listed and the counts are not suspiciously small. For the full inventory, use --format json --all-packages (--all-packages only works with JSON output).
3

Check the exit code

Right after the run, use echo $? in bash or $LASTEXITCODE in PowerShell. The table below gives the meanings.
4

Check the config actually applied

An 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.
5

Check where the offline database lives and how old it is

The local database is read from 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 codeOfficial meaningIn CI
0Packages found, no known vulnerabilities matchedPass
1Packages found, vulnerabilities matchedFail (a finding: fix it, or ignore it with an expiry)
127General errorFail (the scan itself broke)
128No 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 identifier
  • ignoreUntil (optional): after this date it is reported again
  • reason (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.

FlagWhat it does
--offlineFully offline. Judges against a previously downloaded local database, does not update it, and sends no project or dependency information anywhere
--offline-vulnerabilitiesOnly the vulnerability matching is offline; other work (such as transitive resolution) may still use the network
--download-offline-databasesPermits 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 three offline flags. --download-offline-databases does nothing on its own, which causes the error people search for.

The errors and what causes them

  • databases can only be downloaded when running in offline mode → --download-offline-databases was passed on its own; it needs an offline flag alongside it
  • --offline with no local database yet → you get an error saying the database is missing; the first run has to fetch it
  • no package sources found → nothing at that path was recognised as a lockfile or manifest; name it with -L or walk with -r

Choosing without guessing

  • Air-gapped run → --offline (plus --download-offline-databases on the first run)
  • Only the vulnerability data must stay local → --offline-vulnerabilities
  • Pin where the database lives → the OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY environment 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:

v1v2
--experimental-offline--offline
--experimental-download-offline-databases--download-offline-databases
--experimental-call-analysis--call-analysis
--experimental-licenses and relatedmerged 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
  • || true appended to make failures go away
  • One osv-scanner.toml at the repository root, assumed to cover subdirectories
  • Only package.json committed, with no lockfile
  • Assuming --offline still resolves the transitive dependencies of a pom.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.lock or bun.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-packages and 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

ToolData it readsWhat it is good at
osv-scannerOSV.devAcross ecosystems, per lockfile, can run offline
npm audit / pnpm auditnpm advisory databaseImmediate for npm projects, nothing extra to install
DependabotGitHub AdvisoryOpens update PRs — proposes the fix, not just the finding
TrivySeveral (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

FAQ

QDoes osv-scanner support pnpm-lock.yaml?
A

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?
A

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'?
A

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?
A

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?
A

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?
A

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.