npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

osv-scanner-mcp

v0.22.0

Published

MCP server that wraps Google's OSV-Scanner to scan Java, JavaScript, Python, Go, Rust, .NET, and PHP dependencies for known vulnerabilities and recommend fixes

Readme

OSV-Scanner-MCP

CI npm version license node

An MCP server that wraps Google's OSV-Scanner. Ask an MCP client such as Claude to "check this project for vulnerabilities", and it returns the known vulnerabilities (CVE / GHSA) in your dependencies as a report sorted by severity, together with recommended upgrades.

Status: Published on npm (npx -y osv-scanner-mcp). Supports lockfiles and manifests for Java (Maven / Gradle), JavaScript (npm / yarn / pnpm / bun), Python (Poetry / uv / Pipenv / PDM / requirements.txt), Go, Rust (Cargo.lock), and .NET (NuGet: packages.lock.json, obj/project.assets.json, packages.config, *.deps.json, and project files), and PHP (Composer: composer.lock), with upgrade recommendations for Maven, npm, PyPI, Go, crates.io, Packagist, and NuGet. Setup instructions are provided for Claude Code, Claude Desktop, Codex CLI, Antigravity, and VS Code (GitHub Copilot).

Features

  • PHP / Composer (source checkout): scan_project scans composer.lock, including both packages and packages-dev, as Packagist packages. suggest_fix recommends release versions and checks candidates against OSV. Composer and PHP code are never executed. See Composer support.
  • One-shot multi-language scan: Pass a project path to scan_project, and it detects and scans the Java, JavaScript, Python, Go, Rust, .NET, and PHP lockfiles in the project together. Dependencies that could not be scanned (for example, a manifest without a lockfile or an unpinned requirement) are reported up front in coverage
  • Upgrade recommendations: suggest_fix recommends, for each vulnerable package, the upgrade closest to the current release line that addresses its fix targets, for Maven, npm, PyPI, Go, Rust (crates.io), PHP (Packagist), and .NET (NuGet). Known unfixed and informational advisories remain visible. The recommended version is also checked against the OSV database for additional fix-target vulnerabilities
  • Rust without false positives: For Cargo.lock, only crates from crates.io are scanned. Your own workspace crates, git dependencies, and crates from private registries are not looked up as crates.io crates (OSV-Scanner alone would match them by name, and send their names to the OSV API). RustSec notices about unmaintained or unsound crates are labeled informational and counted apart from vulnerabilities
  • .NET without guessing versions: Each project file is matched to its own lockfile or obj/project.assets.json. Without one, only its direct PackageReference items are scanned, and a Version="x" (which means x or higher) is reported as a lower bound. References whose version cannot be known without running MSBuild (properties, conditions, floating versions, Update / Remove) are listed instead of being looked up: OSV treats an unparsable version as older than every release and would report every vulnerability for it
  • Direct and transitive dependencies: Packages are marked as direct or transitive dependencies (from package-lock.json, go.mod, requirements.txt, pom.xml, Cargo.lock, and the .NET files), and update hints are tailored accordingly, for example by naming the direct dependency that pulls in a transitive one
  • Java project scan: scan_java_project scans only the Java (Maven / Gradle) manifests and returns a formatted report
  • JAR/WAR scan: scan_java_artifact detects known vulnerabilities from the metadata inside JAR/WAR archives, for projects without a lockfile or with only shaded/fat JARs (best-effort identification, stated explicitly in coverage)
  • SBOM scan: scan_sbom checks the dependencies recorded in a CycloneDX/SPDX JSON SBOM. It states explicitly that the SBOM's completeness and its match with the actual artifacts are not verified
  • Scan history (opt-in): With OSV_MCP_HISTORY=1, each scan is recorded locally, and scan_history lists past runs and shows what changed since the previous run (new and resolved vulnerabilities, version changes, coverage changes)
  • Severity-sorted reports: Vulnerabilities are grouped by package and sorted by CVSS score, with five severity labels (critical / high / medium / low / unknown) and summary counts
  • Fixed versions: Each vulnerability includes fixed_versions, sorted correctly by Maven version precedence (including non-semver forms such as 2.17.1-RELEASE), Semantic Versioning precedence for npm, Go, and Rust, PEP 440 for PyPI, Composer ordering for Packagist, and NuGet ordering for NuGet
  • Security first: No shell execution, a fixed argument allowlist, path normalization and boundary checks, scanning private copies instead of the original files, and timeouts and output size limits are built in

Composer support

Pass a project directory, composer.lock, or composer.json to scan_project or suggest_fix. Direct/transitive dependency analysis (source checkout) uses the companion composer.json in the lockfile's own directory. A composer.json without a lockfile in the same directory is reported as missing/unconfirmed in coverage.lockfile_missing (a project with no scannable files returns no_manifest_found). An ancestor's lockfile does not establish coverage for an independent child project. vendor directories are not traversed.

The companion's require and require-dev are direct dependencies. Packages reachable through the locked packages' require are transitive; a dependency's own require-dev, suggest, and conflict are not dependency edges. Direct dependencies include their project-relative composer.json in declared_in. introduced_by names each direct root that transitively requires a package (up to 10, with the remainder in introduced_by_omitted). A direct dependency also required by another root remains direct and includes that root in introduced_by; cycles do not introduce a package through itself. Transitive dependencies do not have declared_in, since the root does not declare them directly.

This is name-based graph traversal of the lockfile, not a Composer constraint solver or freshness check. Duplicate installed names, virtual requirements, and references affected by provide/replace are not guessed; unreachable packages remain unknown. Local path and skipped branch entries may connect roots to scanned packages, but their names/metadata are still excluded from scanner input. No path-repository URL or nested package's composer.json is followed. Missing, malformed, symlinked, oversized, or otherwise unreadable companion manifests leave relationships unknown, with a composer_warnings note; locked release packages continue to be scanned. The companion is captured safely once (4 MiB per file), shares the Composer 64 MiB input/JSON structure budgets, and traversal/indexing share a 2,000,000-operation budget across all Composer lockfiles. Relationship uncertainty does not itself make dependency scan coverage incomplete.

The server reads each lockfile safely and scans a private sanitized copy containing only package names and release versions. Repository URLs, dist/source commit references, scripts, and credentials are not passed to OSV-Scanner. Both production and development packages are included. JSON is limited to 16 MiB per lockfile and 64 MiB per scan, with structural work limits. Invalid JSON is excluded; invalid entries and unsupported versions are omitted and reported in skipped_files, making coverage.complete false.

Composer version comparison supports classical numeric releases with one to four components, an optional v prefix, and alpha/beta/RC/stable/patch suffixes (including their short forms). Branch/dev versions, aliases, and other formats such as date-based tags are not evaluated. An advisory with unsupported version boundaries cannot establish a verified fix. Upgrade tiers follow the existing cautious 0.x rules; they do not guarantee compatibility with your constraints or PHP runtime. Update the applicable constraints in composer.json and regenerate the lockfile in a trusted environment; check PHP and extension requirements first. The server does not run Composer or change project files.

Packages with dist.type: "path" are local code: they are excluded before building scanner input, so neither their names nor versions are sent to OSV. They are your own code, so they are counted in coverage.local_composer_packages (like local_crates and local_projects) and do not make coverage.complete false; their own dependencies are listed in composer.lock and scanned. This applies to both production and development dependencies, even when a local package uses the same name and release version as a public package.

Privacy: other private Composer package names and versions are also sent to api.osv.dev. A lockfile alone cannot establish whether a non-path package comes from public Packagist; same-name private packages and VCS forks can produce false positives. coverage.composer_warnings warns when scanned entries lack the standard notification-url (https://packagist.org/downloads/); this is a heuristic, not proof of origin. coverage.complete describes inspected dependency entries, not the lockfile's freshness or its match with composer.json, and does not verify the PHP runtime or extensions.

Requirements

  • Node.js >= 22.13 (development: Node.js 26.12.1, as specified in .node-version)
  • The OSV-Scanner binary — no manual installation needed. If it is not found, a pinned version is downloaded from the official GitHub Releases and verified against a SHA256 checksum embedded in the package before use (cached in ~/.cache/osv-scanner-mcp/)
    • A manually installed binary (on PATH or set with OSV_SCANNER_PATH) is used first
    • Set OSV_MCP_AUTO_DOWNLOAD=0 to disable the automatic download
    • Set OSV_MCP_PREFER_DOWNLOAD=1 to always use the verified downloaded binary instead of one on PATH (recommended for production)
    • The pinned version is osv-scanner 2.7.0. Its binary is cached in ~/.cache/osv-scanner-mcp/v2.7.0/; directories of earlier versions (such as v2.4.0/) are no longer used and can be deleted
  • Scans, suggest_fix, and explain_vulnerability access the network. Vulnerabilities are looked up in the OSV database (api.osv.dev), and scanning pom.xml or requirements.txt also connects to deps.dev (api.deps.dev) to resolve transitive dependencies. See Network destinations and privacy for details and how to turn this off

Setup

Claude Code

claude mcp add osv-scanner -- npx -y osv-scanner-mcp

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "osv-scanner": {
      "command": "npx",
      "args": ["-y", "osv-scanner-mcp"]
    }
  }
}

Codex CLI

codex mcp add osv-scanner -- npx -y osv-scanner-mcp

Or add to ~/.codex/config.toml:

[mcp_servers.osv-scanner]
command = "npx"
args = ["-y", "osv-scanner-mcp"]
startup_timeout_sec = 60   # allow time for the first npx package download
tool_timeout_sec = 300     # default is 60 seconds; allow for the binary download plus a scan (120 seconds by default)

Note: Codex's MCP tool timeout defaults to 60 seconds, while this server's scan timeout defaults to 120 seconds. With the default, Codex may time out first on the first run (which downloads OSV-Scanner) or on larger projects. Raising tool_timeout_sec as above is recommended.

Antigravity

Open MCP Servers → Manage MCP Servers → View raw config in the agent panel and add to mcp_config.json (same format as Claude Desktop):

{
  "mcpServers": {
    "osv-scanner": {
      "command": "npx",
      "args": ["-y", "osv-scanner-mcp"]
    }
  }
}

VS Code (GitHub Copilot)

code --add-mcp '{"name":"osv-scanner","command":"npx","args":["-y","osv-scanner-mcp"]}'

Or add to the workspace's .vscode/mcp.json (also available from MCP: Add Server in the Command Palette):

{
  "servers": {
    "osv-scanner": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "osv-scanner-mcp"]
    }
  }
}

From source

git clone https://github.com/tedorigawa001/OSV-Scanner-MCP.git
cd OSV-Scanner-MCP
npm install
npm run build
# When registering, use `node /path/to/OSV-Scanner-MCP/dist/index.js` instead of `npx -y osv-scanner-mcp`

Environment variables

| Variable | Description | |---|---| | OSV_SCANNER_PATH | Explicit path to the osv-scanner binary. If unset, PATH is searched, then the binary is downloaded automatically. If the path is invalid, the server fails instead of falling back (to avoid running an unintended binary) | | OSV_MCP_ALLOWED_ROOT | If set, scans outside this directory are refused (the boundary against path traversal). Recommended. An empty or whitespace-only value is treated as unset | | OSV_MCP_REQUIRE_ALLOWED_ROOT | If 1 or true, the server refuses to start when OSV_MCP_ALLOWED_ROOT is not set (fail-closed mode for production) | | OSV_MCP_MAX_CONCURRENT_SCANS | Maximum number of concurrent scans (default 2, maximum 16). Requests beyond the limit fail immediately instead of waiting | | OSV_MCP_AUTO_DOWNLOAD | 0 or false disables the automatic binary download (enabled by default) | | OSV_MCP_PREFER_DOWNLOAD | If 1 or true, always use the checksum-verified downloaded binary instead of osv-scanner on PATH (protects against a fake binary planted on PATH; an explicit OSV_SCANNER_PATH still takes precedence) | | OSV_MCP_NO_CANDIDATE_CHECK | If 1 or true, suggest_fix does not query OSV about recommended versions (candidate_check is disabled, and recommended versions are not checked for known vulnerabilities that do not affect the current version) | | OSV_MCP_NO_REMOTE_RESOLUTION | If 1 or true, transitive dependencies of pom.xml and requirements.txt are not resolved through deps.dev. This stops only the requests to deps.dev; package names and versions are still sent to api.osv.dev for the vulnerability lookup. Vulnerabilities in transitive dependencies are then not detected, and responses say so in dependency_resolution.warning. See Network destinations and privacy | | OSV_MCP_GRADLE_DECLARATION_TRUST | Set to registered-helper-v1 to accept author-attested Gradle C1a declaration maps. Unset by default. Read once at server startup. Any other value does not stop the server: it prints a warning to stderr, leaves declaration sources disabled, and adds declaration_trust_config_invalid to the Gradle metadata reasons. See Gradle declaration sources | | OSV_MCP_GRADLE_CATALOG_TRUST | Set to local-catalog-inputs-v1, together with OSV_MCP_GRADLE_DECLARATION_TRUST, to accept declaration sources from contract-v2 version catalog aliases/bundles. Unset by default. Read once at server startup. Any other value prints a warning to stderr, leaves catalog declaration sources disabled, and adds catalog_trust_config_invalid to the Gradle metadata reasons. See Gradle version catalogs | | OSV_MCP_GRADLE_PLUGIN_TRUST | Opt-in convention-plugin trust: local-plugin-build-v1 or local-plugin-loader-attested-v2. Unset disables plugin declaration sources. Read once at startup; unknown values warn and disable. See plugin setup | | OSV_MCP_GRADLE_PLUGIN_TOOLCHAIN_PROFILES | Comma-separated approved toolchain SHA-256 digests (lowercase hex, unique, at most 32). Unset allows none: plugin declaration sources are withheld (plugin_toolchain_not_approved), while verified relations and introduced_by are kept. Read once at startup | | OSV_MCP_GRADLE_PLUGIN_LOADER_PROFILES | Independently approved loader SHA-256 digests, same format. Required only for approved-gradle-loader-v1; does not change strict-policy requirements. A missing approval withholds declaration sources only (plugin_loader_not_approved) | | OSV_MCP_GRADLE_OWNED_BTA_TRUST | Experimental owned BTA diagnostic: owned-bta-offered-inputs-v1. Unset by default; does not reuse other Gradle trust settings. No approved profile ships, so owned metadata is only checked and reported. See Gradle dependency graph | | OSV_MCP_GRADLE_OWNED_BTA_PROFILES | Comma-separated approved owned BTA profile SHA-256 digests (lowercase hex, unique, at most 32). Format-checked only in this release; a malformed value disables owned checks and logs a warning | | OSV_MCP_HISTORY | If 1 or true, records each scan in the local scan history (disabled by default). See scan_history | | OSV_MCP_HISTORY_DIR | Absolute path of the history directory (default $XDG_STATE_HOME/osv-scanner-mcp/history, or ~/.local/state/osv-scanner-mcp/history) | | OSV_MCP_HISTORY_MAX_ENTRIES | Number of runs kept (default 500, maximum 5000). Older runs are deleted, and the total size is also capped at 100 MiB |

Recommendation: The server works without OSV_MCP_ALLOWED_ROOT, but then any absolute path can be scanned. To prevent malicious instructions (prompt injection) from scanning unintended directories, set it to the root of your projects (for example ~/projects). Pass it in the client configuration, for example "env": {"OSV_MCP_ALLOWED_ROOT": "/Users/you/projects"} (in Codex CLI's TOML, use a [mcp_servers.osv-scanner.env] section).

Recommended production configuration: On shared servers, CI, and other production environments, set all three of the following:

  • OSV_MCP_ALLOWED_ROOT=/root/of/scan/targets — fixes the scan boundary
  • OSV_MCP_REQUIRE_ALLOWED_ROOT=1 — refuses to start without a boundary (fail-closed)
  • OSV_SCANNER_PATH=/absolute/path/owned/by/an/administrator or OSV_MCP_PREFER_DOWNLOAD=1 — fixes which binary is run, independent of PATH

Check Network destinations and privacy for where dependency information is sent. In every configuration, package names and versions are sent to api.osv.dev for the vulnerability lookup.

Running with restricted permissions

As an additional layer of defense, you can limit what the server can read and write with Node's permission model (--permission). This is optional and does not change the default way of starting the server. --permission is available in all supported Node versions (22.13 and later). Since npx cannot pass Node flags, start the server with node directly:

{
  "mcpServers": {
    "osv-scanner": {
      "command": "node",
      "args": [
        "--permission",
        "--allow-fs-read=/path/to/OSV-Scanner-MCP",
        "--allow-fs-read=/Users/you/projects",
        "--allow-fs-read=/var/folders/xx/yyyy/T",
        "--allow-fs-read=/private/var/folders/xx/yyyy/T",
        "--allow-fs-write=/private/var/folders/xx/yyyy/T",
        "--allow-fs-read=/opt/homebrew/bin/osv-scanner",
        "--allow-child-process",
        "--allow-addons",
        "/path/to/OSV-Scanner-MCP/dist/index.js"
      ],
      "env": {
        "OSV_MCP_ALLOWED_ROOT": "/Users/you/projects",
        "OSV_SCANNER_PATH": "/opt/homebrew/bin/osv-scanner"
      }
    }
  }
}
  • Read access: the server itself (the directory containing dist and node_modules), the scan targets (OSV_MCP_ALLOWED_ROOT), the temporary directory, and the osv-scanner binary
  • The temporary directory (os.tmpdir(), $TMPDIR on macOS) needs read access under both the symlinked and the resolved path (on macOS, /var/folders/... is a symbolic link to /private/var/folders/...). Write access is needed on the resolved path
  • If you use the automatic download, also allow reading and writing the cache ($XDG_CACHE_HOME/osv-scanner-mcp, by default ~/.cache/osv-scanner-mcp). Otherwise, set OSV_SCANNER_PATH
  • If you enable the scan history (OSV_MCP_HISTORY=1), also allow reading and writing the history directory (OSV_MCP_HISTORY_DIR, or the default under ~/.local/state). The server warns at startup if it is not allowed
  • --allow-child-process is required to run osv-scanner. osv-scanner runs as a child process and is not restricted by the permission model (Node itself warns that this flag weakens the permission model). The impact is limited because osv-scanner only receives verified private copies, but use an OS-level mechanism such as a container if you need stronger isolation
  • On macOS and Windows, --allow-addons is also required for the trusted Koffi native binding used to verify the path of an open file handle. Native code is outside Node's permission sandbox. On Linux, use --allow-fs-read=/proc/self/fd instead; /proc must be mounted. If the OS cannot verify the opened object, boundary-protected reads fail closed. Keep OS-level isolation when scanning in a stronger sandbox.
  • If a required permission is missing, the server prints a warning to stderr at startup, and scans return permission_denied (naming the missing permission and the path)

Network destinations and privacy

Destinations confirmed with osv-scanner v2.4.0 (2026-10-07) and v2.7.0, the pinned version (2026-10-15):

| Operation | Destinations | Information sent | |---|---|---| | Scanning pom.xml (scan_project / scan_java_project / suggest_fix) | api.osv.dev, api.deps.dev | Package names and versions. To resolve transitive dependencies, the names and versions of the dependencies declared in pom.xml (including internal packages) are sent to deps.dev | | Scanning requirements.txt (scan_project / suggest_fix) | api.osv.dev, api.deps.dev | As with pom.xml, the names and versions of the listed dependencies are sent to deps.dev to resolve transitive dependencies. Package indexes given with --index-url and similar options are not contacted | | Scanning lockfiles (gradle.lockfile, npm lockfiles such as package-lock.json, Python lockfiles such as poetry.lock, go.mod, Cargo.lock) | api.osv.dev | Package names and versions (lockfiles already list every dependency, so nothing is resolved remotely). For Cargo.lock, only crates from crates.io are sent: the names of your own crates, git dependencies, and private-registry crates are not sent (confirmed 2026-10-11). For .NET, packages from private NuGet feeds are sent as well: lockfiles do not record which feed a package came from, so they cannot be told apart (a nuget.config in the scanned directory that lists other sources adds a warning to coverage.nuget_warnings). The names of your own projects (type: project and so on) are not sent. Git dependencies in npm lockfiles (package-lock.json, yarn.lock, and so on) are looked up by their repository URL (ecosystem GIT) and commit, so the host name and path of a private git repository (for example https://git.example.com/team/repo) are sent (since osv-scanner 2.6.0) | | scan_java_artifact / scan_sbom | api.osv.dev | Names and versions of the identified packages | | suggest_fix checking recommended versions | api.osv.dev | The names of packages with a recommendation (already sent during the scan) and the candidate versions (published fixed versions). Disable with OSV_MCP_NO_CANDIDATE_CHECK=1 | | explain_vulnerability | api.osv.dev | The vulnerability ID | | Automatic binary download (first run only) | GitHub (official Releases) | Nothing (downloads the pinned binary) |

Both api.osv.dev and deps.dev are operated by Google. In every configuration, the names and versions of scanned packages are sent to api.osv.dev for the vulnerability lookup (offline lookup is not supported).

  • To stop sending data to deps.dev: Set OSV_MCP_NO_REMOTE_RESOLUTION=1. Transitive dependencies of pom.xml and requirements.txt are then not resolved, and only api.osv.dev is contacted. This stops only the requests to deps.dev; requests to OSV continue. Only the dependencies written in those files are scanned, so vulnerabilities in transitive dependencies are missed. Responses of scan_project / scan_java_project / suggest_fix show this as transitive_resolution: "disabled" in dependency_resolution with a warning, so it can be told apart from zero findings. To scan transitive dependencies without deps.dev, use lockfiles (gradle.lockfile, poetry.lock, and so on)
  • Arbitrary repositories are never contacted: osv-scanner's --data-source native mode connects to any URL listed in <repositories> of the scanned pom.xml (a malicious pom.xml could make it contact an attacker's server). This server never uses that mode and sets deps.dev explicitly

Tools

scan_project

Detects the lockfiles and manifests in a project and scans the Java / JavaScript / Python / Go / Rust / .NET / PHP dependencies together. Package managers and builds are never run.

Input

| Parameter | Type | Description | |---|---|---| | project_path | string | Absolute path to the project directory, or to a supported lockfile or manifest (a file given directly is the only file scanned) |

Supported files

| Ecosystem | Files | |---|---| | Java (Maven) | pom.xml, gradle.lockfile, buildscript-gradle.lockfile | | JavaScript (npm) | package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock (text format) | | Python (PyPI) | poetry.lock, uv.lock, Pipfile.lock, pdm.lock, requirements.txt (and variants such as requirements-dev.txt) | | Go | go.mod | | Rust (crates.io) | Cargo.lock (lockfile versions 1 to 4) | | .NET (NuGet) | packages.lock.json / packages.<project>.lock.json, obj/project.assets.json, packages.config / packages.<project>.config, *.deps.json, and *.csproj / *.fsproj / *.vbproj (direct references only, when no lockfile matches) | | PHP (Packagist) | composer.lock (packages and packages-dev; see Composer support) |

.git, node_modules, target, build, .idea, .vscode, .venv, venv, site-packages, __pycache__, .tox, vendor, .cargo, bin and obj in a directory that contains a .NET project file (build output of that project), and symbolic links are not searched. Only the detected files are passed to OSV-Scanner, each with its format given explicitly (coverage.manifests in the response is exactly the scan scope). The search limits are the same as for scan_java_project.

requirements.txt files are not passed to OSV-Scanner as they are. The server parses them, rewrites only the dependency lines it can interpret into simple forms such as name==version, and scans a private temporary copy (deleted after the scan). OSV-Scanner interprets include directives on its own (it even treats - r ../x.txt, with a space, as an include and reads files outside the scan scope), so the copy never contains include directives or options. Includes (-r / --requirement) are expanded by the server, and only when the included file is inside the project directory.

Cargo.lock files are not passed to OSV-Scanner as they are either. OSV-Scanner (v2.4.0 and v2.7.0) ignores where each package comes from and looks every package up as a crates.io crate, so a workspace crate, a git fork, or a private-registry crate with the same name and version as a vulnerable crates.io crate is reported as vulnerable, and its name is sent to the OSV API. The server parses Cargo.lock and scans a private copy that lists only the packages whose source is crates.io (registry+https://github.com/rust-lang/crates.io-index or sparse+https://index.crates.io/), like cargo audit. Packages replaced with [replace] are not scanned, because they are not used (the replacement is classified by its own source). The parser accepts only what Cargo writes; if a Cargo.lock contains anything else, the whole file is excluded and listed in skipped_files instead of being scanned partially. Cargo is never run.

.NET files are not passed to OSV-Scanner as they are either. OSV-Scanner (v2.7.0) looks the project's own projects in *.deps.json up as NuGet packages, and passes .csproj versions such as $(Version) or [1.0,2.0) to OSV as they are; OSV treats a version it cannot parse as older than every release, so every known vulnerability of the package is reported (confirmed 2026-10-23). The server parses every .NET file and scans a private copy in packages.lock.json format that lists only the packages it can look up. Neither dotnet restore nor MSBuild is run, and project files are never evaluated.

  • Matching per project file. For each *.csproj / *.fsproj / *.vbproj, the first source confirmed to belong to it is used: packages.<project>.lock.json; packages.lock.json only when the directory has a single project file; neither, when the project sets NuGetLockFilePath; packages.<project>.config or packages.config under the same rule; then obj/project.assets.json, only when its project.restore.projectPath names this project file (projects in one directory share obj). A lockfile that cannot be matched to a project is still scanned, but no project counts as covered by it
  • Projects without a matching source (including projects with no PackageReference of their own, since Directory.Build.props or the SDK can add references) are scanned from their direct PackageReference items only, and listed in lockfile_missing with status: "direct_only" (or "unconfirmed" when a lockfile exists but cannot be matched). Transitive dependencies are not scanned, so complete is false. Enable RestorePackagesWithLockFile to scan them
  • PackageReference versions. [x] is looked up as x. x, [x,), and [x,y) mean x or higher (NuGet prefers the lowest available version, which may be above x), so they are looked up at x and the packages are marked version_is_lower_bound: true. A lower bound that is excluded or missing, $(...), *, and a missing version (central package management) are not looked up
  • References that might not apply are not looked up: a Condition on the reference, its ItemGroup, or its Version; references inside Choose or Target; names that also have Update or Remove in the file, or in a Directory.Build.props / Directory.Build.targets in the scanned directory; names included more than once; and VersionOverride. They are listed in unresolved_package_references with the reason
  • Runtime packs of self-contained apps in *.deps.json are looked up under their package names (the SDK writes them as runtimepack.<name>, which OSV does not match)
  • Not looked up from restore results: entries of an unknown type, and versions that are not plain NuGet versions (listed in unscannable_nuget_packages). Your own projects are counted in local_projects
  • XML (project files, packages.config, nuget.config) is read by a small parser that rejects DOCTYPE, entity declarations, and CDATA. JSON files use the same structural checks as the npm lockfiles (16 MiB per file). Up to 200,000 packages and 1,000 .NET files are looked up per scan; files beyond the limit are listed in skipped_files. A character reference that is not a valid character (such as &#x110000;) makes the file uninterpretable instead of failing the scan

package-lock.json copies get a resolved URL where it is missing. osv-scanner 2.7.0 treats entries without resolved as local packages and does not look them up, which would silently skip bundled dependencies (inBundle) and registry packages in lockfiles generated with npm's omit-lockfile-registry-resolved=true. The private copy that is scanned gets a public-registry URL for those entries (entries under node_modules/ that are not links and have a plain version, and aliases such as npm:[email protected] in lockfile version 1), so they are looked up as before; the original file is not changed.

npm JSON limits: Normalization of package-lock.json and npm-shrinkwrap.json is limited to 16 MiB per file and 64 MiB per scan, for both input and output. Before parsing, JSON nesting is limited to 128 levels and structural markers (containers, commas, and colons outside strings) to 1,000,000 per file and 2,000,000 per analysis phase. Changed copies use compact JSON, with their size checked before serialization. A lockfile that exceeds these limits is scanned as it is, without filling in resolved (OSV-Scanner parses large lockfiles itself; the limits only bound this server's own parsing and rewriting), and it is listed in coverage.skipped_files with the reason, so complete is false: entries without resolved in it may not have been looked up. Workspace membership and dependency-relation lookups use the same structural checks and remain unconfirmed or unknown when limits are reached.

Reading the output

{
  "project_dir": "/path/to/project",
  "dependency_resolution": { "transitive_resolution": "enabled" },
  "coverage": {
    "complete": false,
    "warning": "…",
    "manifests": [{ "path": "web/package-lock.json", "ecosystem": "npm", "format": "package-lock.json" }],
    "lockfile_missing": [{ "path": "svc/package.json", "ecosystem": "npm", "status": "missing", "hint": "…" }],
    "unpinned_requirements": [{ "file": "py/requirements.txt", "line": 2, "name": "Jinja2", "specifier": ">=2.0", "kind": "lower_bound" }],
    "unscannable_requirements": [
      { "file": "py/requirements.txt", "line": 4, "text": "-e git+https://…", "reason": "…" },
      { "file": "py/requirements.txt", "line": 5, "text": "-r ../shared/base.txt", "reason": "…" }
    ],
    "local_crates": 2,
    "unscannable_crates": [{ "file": "Cargo.lock", "name": "my-fork", "version": "1.0.0", "reason": "Git dependency: …" }],
    "skipped_files": []
  },
  "ecosystem_breakdown": { "npm": { "manifests": 1, "vulnerable_package_count": 2, "vulnerability_count": 4 } },
  "vulnerable_package_count": 2,
  "vulnerability_count": 4,
  "severity_breakdown": { "critical": 0, "high": 2, "medium": 2, "low": 0, "unknown": 0 },
  "packages": [
    { "name": "minimist", "version": "1.2.5", "ecosystem": "npm", "dependency_groups": ["dev"], "dependency_relation": "direct", "declared_in": ["package.json"], "vulnerabilities": [] },
    { "name": "qs", "version": "6.7.0", "ecosystem": "npm", "dependency_relation": "transitive", "introduced_by": ["express"], "vulnerabilities": [] }
  ]
}
  • Always check coverage. If complete is false, some dependencies were not scanned, so zero findings does not mean the project is safe
    • lockfile_missing: manifests without a lockfile (package.json, pyproject.toml, Pipfile, setup.py, build.gradle, Cargo.toml, and so on). Not recorded if a lockfile of the same ecosystem is in the same directory. If there is only a lockfile in a parent directory, the manifest is not recorded only when package-lock.json (v2 or later) is confirmed to contain that directory (npm workspaces), when pnpm-lock.yaml (formats 5.3/5.4/6.0/9.0) contains its exact normalized importer path (pnpm workspaces; source checkout), when Pipenv declared/locked local paths and captured child declarations can be confirmed (see below; source checkout), when PDM locked local identity and captured workspace/path declarations can be confirmed (see below; source checkout), when Poetry locked path-package identity and exported child declarations can be confirmed (see below; source checkout), when uv local workspace identity and manifest membership can be confirmed (source checkout), when bun.lock contains its exact normalized workspace path (text formats 0/1/2; source checkout), when Yarn workspace membership and captured declarations can be confirmed (see below; source checkout), or when Cargo.lock contains a local crate with the [package] name of the Cargo.toml and the directory is a member of the workspace according to members / exclude of the Cargo.toml next to Cargo.lock (Cargo workspaces; members globs with * and ? are supported). A crate with the same name that cannot be confirmed as a member, such as a path dependency outside the workspace, is recorded as unconfirmed. Library crates often do not commit Cargo.lock, so their Cargo.toml is recorded here; generate one with cargo generate-lockfile. Otherwise it is recorded with status: "missing", or status: "unconfirmed" for formats that cannot be checked (unsupported Yarn layouts, unsupported Python layouts, and so on). Generate the lockfile as described in hint (in a trusted environment) and scan again
    • unpinned_requirements: lines in requirements.txt that do not pin a version. kind is unpinned (no version), range (>, <, !=, ==1.*, combined ranges, and so on), or lower_bound (>=, ~=). OSV-Scanner does not scan unpinned and range lines, and scans lower_bound lines at the lower bound (those packages are marked version_is_lower_bound: true and may differ from the installed version)
    • unscannable_requirements: lines that are not scanned, with the reason: -e, name @ URL, paths, includes that were not expanded (outside the project directory, missing, URLs, or over the limits), constraint files (-c, not applied), and options or version specifiers that cannot be interpreted. Lines that cannot be interpreted are recorded here instead of being ignored
    • local_crates (only when a Cargo.lock was scanned): the number of crates without a source in Cargo.lock (workspace members and path dependencies). They are your own code and are not scanned; they do not make complete false. A third-party crate vendored as a path dependency is not scanned either (as with cargo audit)
    • unscannable_crates (only when a Cargo.lock was scanned): git dependencies (their versions do not correspond to crates.io releases) and crates from registries other than crates.io (a crate with the same name on crates.io may be a different crate). They are not scanned, so complete is false. Source URLs are not included in the response
    • local_projects, unscannable_nuget_packages, unresolved_package_references, nuget_warnings (only when a .NET file was found): see the .NET notes above. nuget_warnings notes lower bounds, a possibly stale project.assets.json (more than a minute older than its project file; smaller differences are ignored because a clone writes files in any order), references that cannot be checked from project files alone (SDK-implicit references and Directory.Build.* outside the scanned directory), and package sources other than nuget.org. Entries in unscannable_nuget_packages and unresolved_package_references make complete false
    • skipped_files: files excluded from the scan, with the reason (a Cargo.lock that cannot be interpreted with certainty, a requirements.txt that cannot be read or is larger than 1 MiB, or a pom.xml whose parent POM chain references a file outside the allowed root). A pom.xml scanned without its parent POM, because the parent could not be read, is also listed here with the reason (see Parent POMs)
    • Each list holds up to 200 entries; the number of omitted entries is returned in omitted_items
  • ecosystem_breakdown includes ecosystems with zero findings, to show that they were scanned
  • Informational advisories (Rust): RustSec also publishes notices that are not vulnerabilities: unmaintained (the crate is no longer maintained), unsound (a soundness bug), and notice. Most have no fixed version. When every advisory in a group is such a notice (every related ID, including aliases, must have its details available and be a notice of a known kind; otherwise it is treated as a vulnerability), the vulnerability has informational set to its kind, is listed after the vulnerabilities of the package, and is counted in informational_count (and in ecosystem_breakdown) instead of vulnerability_count, severity_breakdown, and vulnerable_package_count. If another database (such as GitHub's) lists the same issue as a vulnerability, it stays a vulnerability. informational_count appears only when there is at least one
  • dependency_relation shows whether a package is a direct (direct) or transitive (transitive) dependency. OSV-Scanner does not report this, so the server determines it from validated manifest data and scanned copies:
    • package-lock.json (v2 or later): dependencies of the root and workspace package.json files, resolved with Node's lookup rules (nested node_modules first, then parent directories), are direct; everything reachable from them is transitive. Direct dependencies list the package.json files that declare them in declared_in; transitive dependencies list the direct dependencies that require them in introduced_by (up to 10, with the rest counted in introduced_by_omitted). A version that is both a direct dependency and required by another dependency is direct and also has introduced_by
    • go.mod: require lines without // indirect are direct. Modules affected by a replace directive are marked replaced_in_go_mod: true (OSV-Scanner reports the replacement module and version)
    • requirements.txt: dependencies written in the file are direct; dependencies resolved through deps.dev are transitive
    • pom.xml: OSV-Scanner reports the dependencies declared in pom.xml (and its parent POMs) and the transitive dependencies resolved through deps.dev as separate results (source.type lockfile / unknown), and this split is used (so parent POMs, profiles, properties, and dependency management are interpreted exactly as OSV-Scanner does). introduced_by / declared_in are not available. This relies on undocumented OSV-Scanner output, so anything unexpected is reported as unknown
    • Cargo.lock: crates.io dependencies immediately required by a local/workspace/path crate are direct; crates.io dependencies reachable through them (including git and alternate-registry origins) are transitive. introduced_by names the first direct dependency on each path. References are resolved by exact name, version, and source where provided; ambiguous references are not guessed. Replacements are followed. declared_in lists project-relative Cargo.toml paths only when every declaring crate has a unique matching manifest. Otherwise declaring_crates names the unmapped declaring crates (up to 10, with the rest in declaring_crates_omitted). Dev/build/target-specific distinctions are unavailable. Traversal over 2,000,000 edges returns unknown
    • .NET: Direct / Transitive in packages.lock.json; the dependencies of project.frameworks in project.assets.json; the dependencies of the project library in *.deps.json; and every reference read from a project file are direct. packages.config does not say, so it is unknown. Origin/declaration enrichment is available in the source checkout as described below.
    • Composer (source checkout): the companion root's require / require-dev are direct, and locked require reachability establishes transitive dependencies. declared_in names the companion for direct dependencies; introduced_by lists transitively requiring direct roots. See Composer support for ambiguity and work limits.
    • pnpm-lock.yaml (source checkout): lockfile formats 5.3, 5.4, 6.0 and 9.0 are supported. Importer dependencies, devDependencies and optionalDependencies are direct, with the lockfile's declaring importer package.json paths in declared_in. Locked dependency and optional-dependency edges establish introduced_by and transitive reachability. Workspace importers, npm aliases, scoped packages, multiple origins and peer-context-specific snapshots are supported. A package that is both direct and transitively required remains direct and also lists its origins. Local/Git references, unresolved identities and unreachable packages do not establish relationships; links and source files are never followed. Declaration paths reflect the lockfile, not independently verified current manifests. Workspace coverage (source checkout) uses the exact normalized importer path relative to an ancestor pnpm-lock.yaml: included children are covered, absent importers are missing, and unsupported/malformed/ambiguous or analysis-limited importer metadata is unconfirmed. Empty importer maps and Unicode/space paths are supported. Importer presence does not prove freshness or agreement with current package.json dependencies. Only lockfiles found inside the selected scan directory are considered. Coverage and vulnerability scanning use the same captured lockfile bytes (16 MiB per file, 64 MiB total); unreadable or oversized inputs are skipped with a reason. Each lockfile is parsed with its own budget sized from its length (1,024 operations per KiB, at least 2,000,000), so files within the size limits are not rejected because other lockfiles were parsed first; importer indexing shares a 2,000,000-operation budget per detection, separate from relationship analysis. The lockfile is read from YAML's syntax tree in linear time (only strings, maps, and sequences; a 28,000-package lockfile takes about a second, a 16 MiB one about 3 seconds). Unsupported/malformed YAML, duplicate keys, anchors, aliases, tags, multi-document files, conservative nesting limits, more than 1,000,000 lines, inputs above 16 MiB, or more than 2,000,000 relationship operations per scan (shared by all pnpm lockfiles) leave relationships unknown; scanning continues. The existing shared relationship input byte limit and 10-origin output limit apply.
    • yarn.lock (source checkout): Classic v1 and Berry metadata versions 4/5/6/8 support direct/transitive relations, introduced_by and declared_in. Classic uses captured root/workspace package.json declarations; Berry uses exact locked descriptors and workspace: entries. Dependencies and optional dependencies are followed; peers are not independently guessed or installed. Multiple selectors, scoped/npm aliases, cycles, multiple roots and direct/transitive overlap are supported. Local/Git/patch/portal/virtual identities that cannot establish a registry release remain unknown and are never followed to source files.
    • bun.lock (source checkout): text formats 0/1/2 support direct/transitive relations, introduced_by, declared_in and workspace coverage. Locked workspace declarations are direct; dependencies, optional dependencies and installed peers are followed through exact package placement. Nested versions, scoped names, npm aliases, cycles and direct/transitive overlap are supported. Intentionally unresolved optional dependencies are not guessed from another installed package.
    • uv.lock (source checkout): version 1, revisions 0–5 support direct/transitive relations, introduced_by, declared_in and workspace coverage. Captured local workspace packages define roots from normal, optional and development-group dependencies. Registry identities resolve only when their normalized name and any locked version/source constraints identify a unique package. Optional edges are followed only for explicitly requested extras; unknown combinations of differing markers are not joined.
    • poetry.lock (source checkout): lock versions 1.1/2.0/2.1 support direct/transitive relations, introduced_by, declared_in and conservative child coverage. Captured root and confirmed local child pyproject.toml declarations establish direct roots; unique locked package names and unconditional string dependency edges establish transitive origins. Modern project dependencies/optional dependencies, Poetry dependency/dev/group tables and standard dependency groups are supported. Ambiguous names, conditional/optional edges requiring a solver, nonregistry releases and unreachable packages remain unknown.
    • pdm.lock (source checkout): 4.0–4.4 lock schemas and 4.5.0/4.5.1 support direct/transitive relations, introduced_by, declared_in and child coverage. Captured project dependencies, optional groups, standard dependency groups (including group includes) and legacy tool.pdm.dev-dependencies define roots, selected by metadata.groups when present. Explicit extras select exact locked extras identities; ambiguous names/versions/sources are not guessed. PDM-generated editable dev declarations (-e file:///${PROJECT_ROOT}/...#egg=name) use the explicit egg identity and exact safe locked local path. Unsupported declarations do not discard other known roots; an unsupported runtime declaration still prevents confirming child coverage.
    • Pipfile.lock (source checkout): spec 6 uses captured Pipfile declarations in packages, dev-packages and custom categories to identify direct registry releases and their declared_in. Other locked registry entries in a category with complete matched declarations and at least one root are transitive. This format has no dependency edges: introduced_by is supplied only when every category that locks the release without declaring it has one effective root; if any such category has several possible origins or incomplete declarations, the origin is omitted rather than reported partially. A package direct in one category and transitive in another remains direct and can retain a known origin.
    • Gradle (source checkout): an optional pre-exported graph supplies direct/transitive relations and introduced_by. Without usable metadata the relation remains unknown.
    • Other formats, npm lockfile version 1, and entries not reachable from the project are unknown. If lockfiles disagree, the value is mixed. When the same package is found in several files, introduced_by is the union of their origins only if every file that does not report it as direct names its origin; if any of them is unknown or transitive without a known origin, introduced_by is omitted rather than listing only part of the origins. declared_in is checked independently: every source must have a complete direct declaration list or establish that it is transitive with no direct declaration. A direct source without declaration paths, an unknown source, unmapped declaring crates, or an explicit completeness failure suppresses the entire declared_in list. Fully known paths are deduplicated and sorted; a proven transitive source does not suppress them. Default update filenames are guidance and are not substituted for missing declaration evidence. This metadata limitation alone does not lower vulnerability coverage

Pipenv origins and child coverage (source checkout): packages maps to locked default, dev-packages to develop, and custom categories retain their names. The companion must be readable and declarations must match captured locked identities before a category can establish transitivity. Missing/incomplete declarations leave residual packages unknown; lock freshness and runtime compatibility are not globally verified. Local/Git/URL entries are not classified as public registry releases by relationship metadata. This does not change OSV's existing package-origin handling or communication policy.

A child pyproject.toml can be covered only if a root Pipfile path declaration and a unique locked entry agree on its exact safe relative directory and normalized package name, and all static runtime requirements are present at compatible locked releases in the same category. Basic exact/inequality PEP 440 constraints are checked; unsupported constraints, markers, extras, dynamic metadata, nonempty optional/development groups (including Poetry group and legacy dev tables) and legacy backend declarations remain unconfirmed. The flat lock does not record a local package's version or exported dependency graph; these checks establish captured declaration inclusion, not backend metadata equivalence or lock freshness. Independent child Pipfile, setup.py and setup.cfg files are never assumed to be exported by a path package. Absent child paths are missing; locked paths with unverifiable declarations are unconfirmed. Same-directory lock coverage retains its existing convention.

Only detected pyproject.toml companions and the root Pipfile are captured; source paths are never followed to read arbitrary files. Pipenv, Python, plugins and build backends are not run during scanning. Strict JSON parsing rejects duplicate keys, invalid syntax and nesting above 128; lock capture is limited to 16 MiB/file and 64 MiB total, companions to a separate 4 MiB/file and 64 MiB total. Coverage and relationship indexing have independent 2,000,000-operation budgets. OSV-Scanner reads only default/develop, so nonempty custom categories are additionally scanned as separate temporary default-category copies, preserving different versions without overwrites and restoring original category names in dependency_groups for both scan reports and fix suggestions (64 MiB and 200 generated copies per scan). Copy limits or malformed custom metadata are reported in skipped_files and make coverage incomplete. Source files remain unchanged. pylock.toml, custom lock filenames, installed-environment graph inspection and full multi-root origin resolution are outside this support.

PDM origins and child coverage (source checkout): native [tool.pdm.workspace].members patterns (exact paths, *, ?, whole-segment **, including dot directories) are checked against exact safe locked path identities. Explicit reachable local path dependencies can also establish inclusion. A matching name alone does not cover a child. Captured child name/version and exported runtime/optional declarations must agree with unique locked dependency identities (specifier order is normalized; marker token types, boundaries and literal contents are preserved while ignoring whitespace outside literals and quote style; other marker or constraint differences are not solved); missing local directories are missing, while unreadable, unsupported, ambiguous or unverifiable declarations are unconfirmed. Unsupported glob syntax, child development groups and dynamic dependencies/optional-dependencies cannot establish complete child coverage. Other child markers (setup.py, setup.cfg, Pipfile) remain unconfirmed; same-directory locks retain the existing coverage convention. Group presence and membership do not constitute a full freshness check.

declared_in names captured root/confirmed-child pyproject.toml files. PEP 503 names, PEP 440 release comparison, multiple origins, cycles, direct/transitive overlap and distinct extras identities are supported. Dependency markers are not evaluated; differing expressions on the same traversal path are not joined. No PDM, plugin, build backend or project code runs during a scan. Local/Git/URL identities are not treated as registry releases by relationship analysis; local paths are never followed to read source files. Existing OSV communication and package-origin handling are unchanged. TOML parsing is bounded and locks are captured once for metadata and scanner input (16 MiB per file, 64 MiB total). Captured companions have separate 4 MiB/file, 64 MiB total limits. Coverage and relationships have separate shared 2,000,000-operation budgets; exhausting relationship analysis preserves confirmed membership. Unsupported metadata leaves relationships unknown while vulnerability scanning continues. pylock.toml, custom lock filenames and full constraint/environment solving are outside this support.

Poetry origins and child coverage (source checkout): Poetry path dependencies are used to check shared-lock monorepos. This does not assume uv-style workspace membership. A child must have an exact safe relative directory source in the ancestor lock, be reachable from captured root declarations, and match a unique locked local name/version. Its runtime/optional declarations must agree with unique locked dependency edges (matching constraint text or exact locked versions); unsupported constraints or source identities remain unconfirmed. A path dependency on another local package (including a sibling such as ../core) agrees when the locked edge is an unconditional {path, develop} table resolving to that package's locked directory inside the lock directory. Child development groups are not exported by path packages and therefore make child coverage unconfirmed. Dynamic project.dependencies or project.optional-dependencies also leave child coverage unconfirmed because backend-generated declarations cannot be verified from captured TOML. An absent child directory is missing; unreadable/unsupported/ambiguous metadata is unconfirmed. Independent descendants with the same package name do not count as included. Legacy setup.py/setup.cfg/Pipfile children remain unconfirmed. A same-directory lock retains the existing coverage convention.

Root declarations include all supported development/optional groups, because scanning covers the universal lock. Unconditional string edges are followed even when the root or an intermediate package is marked optional; conditional or optional dependency tables still remain unresolved. Names use PEP 503 normalization and versions use PEP 440 comparison. No environment, extras or constraint solver is run; lock freshness is not globally verified. Declared paths come from safely captured companions, not from following arbitrary lockfile source paths. Poetry, plugins and build backends are never executed during scanning. Lock bytes are captured once for coverage, relationships and scanner input (16 MiB/file, 64 MiB total); companions have a separate 4 MiB/file, 64 MiB total budget. TOML preflight and independent shared 2,000,000-operation coverage/relationship budgets bound metadata analysis; exceeding only the relationship budget preserves confirmed coverage. Private-index exclusion is not established by this metadata analysis.

uv origins and workspace coverage (source checkout): local editable, virtual or directory paths are matched with [manifest].members names to confirm child workspace membership. A unique local root at . can be confirmed for single-project locks. A local path dependency alone does not establish workspace membership; a locked local package that is not a confirmed member (such as a path dependency), or missing or ambiguous membership evidence, remains unconfirmed, while a child absent from the lockfile is missing. Unicode/space paths and virtual projects are supported. Only pyproject.toml children are confirmed; legacy setup.py/setup.cfg/Pipfile markers remain unconfirmed against ancestor uv locks.

declared_in reflects locked workspace pyproject.toml paths; current source files and lockfile freshness are not compared. Names use PEP 503 normalization; explicit dependency versions use PEP 440 comparison and source identities are kept separate. No marker, Python-version, platform or package constraint solver is run. Different marker expressions on one path are conservatively left unresolved rather than joined, and unreachable/ambiguous packages remain unknown. Workspace groups and optional declarations are included because scanning covers the universal lockfile, not a selected environment. Local paths may be graph intermediates but are never followed to read or execute source files. uv and build backends are never executed during scanning.

The same safely captured bytes feed membership, relation metadata and the scanner. TOML parsing is bounded at 16 MiB per file / 64 MiB total with a conservative preflight limit of 128 container levels, 128 unquoted dots per line and a per-file structural-character budget sized from its length (1,024 per KiB, at least 2,000,000), so files within the size limits are not rejected for size alone; duplicate keys and unsupported schema are rejected for metadata analysis. Workspace indexing and graph traversal use separate shared 2,000,000-operation budgets, so relationship limits do not invalidate confirmed membership; a root declared by several members with the same extras and marker is traversed once. Unreadable/oversized files are skipped with a reason. Package origin and private index exclusion are not established by relationship analysis; existing OSV communication behavior is unchanged.

Bun origins and workspace coverage (source checkout): the exact normalized workspaces path in captured bun.lock establishes inclusion in an ancestor lockfile, including Unicode/space paths and empty entries. An absent child is missing; unsupported/malformed/duplicate/ambiguous or parse-limited data is unconfirmed. Declaration paths reflect the lockfile, not a comparison with current package.json contents, and do not establish freshness or compatibility. Package paths resolve from the declaring workspace/package toward the locked root while keeping scoped names intact; unsupported local/Git/tarball identities are barriers and are never followed to source files. Bun is not executed by the server.

JSONC parsing allows comments/trailing commas and rejects duplicate keys, syntax errors and nesting beyond 128 levels before constructing oversized structures. Captured lockfiles are limited to 16 MiB each / 64 MiB total. Each lockfile is parsed with its own budget sized from its length (1,024 operations per KiB, at least 2,000,000); workspace indexing and relation traversal have separate shared 2,000,000-operation budgets per scan; relation exhaustion leaves metadata unknown without changing confirmed coverage. Coverage and scanning use the same captured bytes. Unreadable/oversized files are skipped with a reason. Binary bun.lockb is unsupported; generate a text lockfile in a trusted environment with bun install --save-text-lockfile --lockfile-only --ignore-scripts. Only lockfiles inside the selected scan directory are considered.

Yarn workspace coverage (source checkout): Classic does not contain workspace importer records, so a captured private root's workspaces array (or workspaces.packages) must include the child and its captured dependency descriptors must resolve in the lockfile or to a uniquely named configured local workspace satisfying the requested SemVer range. Berry matches exact normalized workspace: resolution paths and checks captured dependency declarations against the locked workspace. Classic children need a nonempty name and usable version; manifests without them are not workspace members. Wildcards do not consume dot-prefixed path segments unless the pattern explicitly starts that segment with a dot. *, ?, whole-segment **, exclusion patterns, one trailing / (as Yarn strips it), and Unicode/space paths are supported; other empty segments are unconfirmed; brace/extglob/character-class patterns remain unconfirmed. Independent children are missing; unreadable declarations, unresolved/ambiguous entries and analysis limits are unconfirmed. Coverage confirmation does not prove lockfile freshness or platform compatibility.

Yarn lockfiles and manifests are captured once within the selected scan boundary; coverage, relation metadata and scanning use those same inputs. Yarn is never run during a scan, and links, configuration plugins and source paths are not evaluated. Lockfiles are limited to 16 MiB each / 64 MiB total; captured package manifests to 4 MiB each / 64 MiB total with bounded JSON parsing. YAML nesting limits and a 1,000,000-line limit apply. Each lockfile is parsed with its own size-based budget (1,024 operations per KiB, at least 2,000,000); workspace coverage shares one 2,000,000-operation budget per scan, and relationship analysis has a separate 2,000,000-operation budget, so exceeding the relationship limit leaves relations unknown without making confirmed coverage unconfirmed. Unsupported metadata leaves relations unknown and membership unconfirmed; unreadable/oversized lockfiles are skipped with a reason. A selected lockfile alone does not cause child manifests or files outside the selected scan directory to be searched. Berry declaration paths may come from locked workspace entries when companions were not captured.

NuGet origins and declarations (source checkout): introduced_by follows dependency edges from direct NuGet packages in each locked TFM or assets/deps target separately, then merges origins only for the same package/version in the report. Case-insensitive IDs and equivalent normalized versions match. packages.lock.json and assets targets contain a resolved solution, so edges resolve a unique installed name within that target; dependency constraints are not solved again. deps.json edges additionally require an exact normalized version match. Multiple origins and cycles are supported; a root does not introduce itself. Duplicate/ambiguous names a