Skip to content

CLI Reference

The packageprobe command-line interface is a thin wrapper over the same Rust engine as the macOS app — every scan, policy evaluation, and export produces identical results. The short alias pkgprb is interchangeable with packageprobe.

Run packageprobe --help, or packageprobe <command> --help for any subcommand, to see this reference from the binary itself.

packageprobe [GLOBAL OPTIONS] <COMMAND> [ARGS]

The command-line binary is unlocked by the CLI add-on (CLI entitlement), separate from the Base app. Every work command (scan, scan-image, sbom, sarif, vex, sbom-diff, fix) first requires the CLI add-on; without it the command prints an upgrade prompt and exits 3. On top of that, each work command gates on its own capability entitlement:

  • scan and scan-image require SCANNING (Base). --policy also requires POLICY.
  • sbom, sarif, and sbom-diff require SBOM (Compliance pack).
  • vex requires VEX; fix requires CVE_FIX.

The account/inspection commands (trial, login, logout, license, config, audit-verify) are exempt from the CLI gate so you can always activate. During the 7-day trial everything is unlocked.

--config, --license-file, and --help apply to every subcommand; -V/--version is accepted only at the top level.

OptionTypeDescription
-c, --config <PATH>pathConfig file to use. Default: ~/.packageprobe/config.yaml.
--license-file <PATH>pathValidate against a license from this file instead of ~/.packageprobe/license.json. Overrides the PACKAGEPROBE_LICENSE_KEY stateless path for the entitlement check. Ignored by trial/login/logout/config/audit-verify (which manage the license file themselves). Note: license refresh always re-fetches and rewrites the default ~/.packageprobe/license.json, not the override.
-h, --helpflagPrint help. Works on any subcommand (packageprobe <command> --help).
-V, --versionflagPrint version. Top-level only — packageprobe --version; it is not accepted on subcommands.

Every subcommand uses one differentiated exit-code taxonomy — a stable CLI surface. Higher codes outrank lower ones (an incomplete gate can never masquerade as a clean violation).

CodeNameMeaning
0OKThe work completed and no requested gate failed. For a gate: complete evidence, no violations (PASS). Not a claim that the scan found nothing — see the caution below.
1ViolationFindings failed a gate — a policy violation, new --alerts issues since the last scan, a fix whose advisory survived a completed re-check, or a broken audit-verify hash chain.
2IncompleteNo trustworthy verdict. Either incomplete/absent evidence (an incomplete scan, a policy rule whose vulnerability evidence is absent or degraded, an audit log with no records, a fix whose verification could not run) or a bad invocation (a clap usage error, a rejected flag value such as --format/--backend/--scope, or a fix argument that isn’t a directory).
3OperationalThe command could not run at all — config unreadable, scan couldn’t execute, an alert check that could not reach its data, a licence-server call that failed, a package manager that errored, or a missing entitlement (the CLI add-on or the command’s own capability).

The invariant: a command exits 0 only when the work completed and no gate you asked for failed. A failure to run is never 0 (that would be a false all-clear) and never 1 (that would masquerade as a finding). So a CI step can branch: 0 no requested gate failed · 1 fix the finding · 2 re-check the invocation or add --vulns · 3 fix the environment/licence. A step that keys on “non-zero == failure” is unaffected.

The account commands (trial, login, logout, license, config) exit 0 on success and 3 on error — they produce no findings, so a failure there is always operational.

Scan the configured paths for projects and their dependencies.

Terminal window
packageprobe scan [OPTIONS]
FlagTypeDefaultDescription
-o, --output <PATH>pathstdoutWrite the JSON report to a file instead of stdout.
--vulnsflagfalseEnrich dependencies with vulnerability data from OSV (via the vuln-proxy). Slower on first run.
--vuln-cache-ttl <HOURS>u6424Cache TTL for vulnerability data, in hours.
--policyflagfalseEvaluate configured policy rules and set the exit code accordingly. Requires POLICY. A vuln-dependent rule without --vulns fails closed (exit 2).
--enrich-for-policyflagfalseWhen --policy needs vulnerability evidence, auto-enable enrichment for this run instead of failing closed. Still fails closed if that enrichment is degraded.
--alertsflagfalseReport new vulnerabilities/violations since the last scan. Exits 1 when new alerts are found, 0 when the diff ran and found none (including the first scan, where there is no baseline), and 3 when the alert check returns an error or its config can’t be read. An unreadable or malformed previous scan cache (~/.packageprobe/scan-cache-prev.json — corrupt, truncated, empty, wrong-shape, permission-denied, or a dangling symlink) also exits 3, not 0: it is a baseline that exists but could not be read, which is not the same as a genuine first run and is never an all-clear (PKG-819).
--scan-concurrency <N>usizeconfig / min(cpus, 8)Max concurrent project-discovery tasks. 1 forces sequential.
--backend <BACKEND>stringnativeDiscovery backend. Only native is accepted — any other value is rejected and the command exits 2. (The legacy config field discovery_backend: maps arbitrary values to native with a warning; the CLI flag does not.)

The JSON output is a ScanEnvelope: schema_version, scanned_at, a results object keyed by stable project id, and coverage/completeness fields (scan_incomplete, enrichment_coverage, disabled_ecosystems, …). See the data model for the full shape.

Terminal window
packageprobe scan # fast, no network
packageprobe scan --vulns # + OSV enrichment
packageprobe scan --vulns --policy # + policy gate (exit 1 on violation)
packageprobe scan --vulns -o report.json

Scan a container image from a registry or a local tarball — no Docker daemon required (in-process OCI pipeline).

Terminal window
packageprobe scan-image <IMAGE> [OPTIONS]
  • <IMAGE> — a registry ref (nginx:1.25, ghcr.io/owner/img:tag) or a local tarball (oci-archive:./image.tar, docker-archive:./image.tar).
FlagTypeDefaultDescription
--vulnsflagfalseEnrich discovered artifacts with vulnerability data.
--vuln-cache-ttl <HOURS>u6424Cache TTL for vulnerability data, in hours.
--policyflagfalseEvaluate policy against the image and set the exit code. Requires POLICY; severity rules need --vulns.
--enrich-for-policyflagfalseAuto-enable enrichment when --policy needs it, instead of failing closed.
--base-image-suggestionsflagfalseCompare the base image against lighter/patched variants and recommend one with fewer CVEs. Requires --vulns; each candidate is its own scan (2–3× the time).
--platform <OS/ARCH[/VARIANT]>stringlinux/amd64Platform for a multi-arch image (e.g. linux/arm64, linux/arm/v7). Fails visibly if the image doesn’t ship it. Pass all to scan every platform the (registry) image ships, one result each.
-o, --output <PATH>pathstdoutWrite JSON output to a file.
Terminal window
packageprobe scan-image nginx:1.25 --vulns
packageprobe scan-image oci-archive:./img.tar
packageprobe scan-image myimg:tag --platform linux/arm64 --vulns --policy

Generate an SBOM for a project from the last cached scan. Requires SBOM.

Terminal window
packageprobe sbom <PROJECT> [OPTIONS]
  • <PROJECT> — the project name (as shown in scan output) or its stable id.
FlagTypeDefaultDescription
-f, --format <FORMAT>stringcyclonedxcyclonedx (CycloneDX 1.6) or spdx (SPDX 2.3).
-o, --output <PATH>pathstdoutWrite to a file.
--workspace-shape <SHAPE>enumautoauto (collapse when the project is part of a multi-package workspace), collapsed (always one SBOM for all workspace members), or per-member (just this project).
Terminal window
packageprobe sbom my-app --format cyclonedx -o my-app.cdx.json

Generate a SARIF 2.1.0 log of vulnerability findings from the last cached scan (for GitHub Code Scanning). Covers all projects. Requires SBOM (Compliance pack).

Terminal window
packageprobe sarif [-o results.sarif]
FlagTypeDefaultDescription
-o, --output <PATH>pathstdoutWrite to a file.

Generate a VEX document for a scanned project. Requires VEX.

Terminal window
packageprobe vex <PROJECT> [OPTIONS]
FlagTypeDefaultDescription
-f, --format <FORMAT>stringcyclonedx-vexcyclonedx-vex, openvex, or csaf.
-s, --scope <SCOPE>enumpostureposture (a statement per finding) or exceptions (only triaged, non-affected findings).
-o, --output <PATH>pathstdoutWrite to a file.

Diff two CycloneDX SBOMs and report added / removed / changed components. Requires SBOM.

Terminal window
packageprobe sbom-diff <PREVIOUS> <CURRENT> [OPTIONS]
FlagTypeDefaultDescription
-f, --format <FORMAT>stringprettypretty, markdown, or json.
-o, --output <PATH>pathstdoutWrite to a file.

Fix vulnerable dependencies by upgrading to their fixed versions. Requires CVE_FIX (Developer pack).

Terminal window
packageprobe fix <PROJECT> [OPTIONS]
  • <PROJECT> — a path to the project directory, or a project name / stable prj_… id from the last cached scan (the same argument sbom and vex accept).
FlagTypeDefaultDescription
--cve <ID>string—Scope preview/apply to a specific CVE/GHSA (e.g. CVE-2021-23337).
--applyflagfalseApply the fixes. Default is a dry-run preview.
--allflagfalseFix all fixable vulnerabilities in the project.

How <PROJECT> is resolved. fix --apply modifies whatever it resolves to, so the arbitration is deterministic and the resolved directory is echoed as a Target: line on stderr before anything runs:

  1. An existing directory always wins. The path form is unchanged, with or without a scan cache on disk.
  2. …unless the same string also names a different scanned project. That is genuinely ambiguous, so it is refused (exit 2) rather than guessed. The error names both readings and the spelling that resolves each — ./api for the directory, the project’s prj_… id for the project.
  3. Otherwise the argument is looked up as a project: stable id, then name. A name shared by two scanned projects is refused, not picked — pass the prj_… id.
  4. A project whose recorded path no longer exists is a stale-cache error (exit 2), never silently “no such project” — re-run packageprobe scan. So is a project whose path was recorded relative (a paths: entry of . records ./repoA, verbatim): a relative path names a different directory from every different working directory, and the scan’s own working directory is not recorded, so resolving it here could target an unrelated project that merely has the same layout. Pass the project’s path, or re-scan with an absolute paths: entry.
  5. An argument matching neither reading exits 2. It never falls back to a default target, the working directory, or “all projects” — so an unset variable (packageprobe fix "$PROJECT") fails loudly instead of acting.
  6. A polyglot root — one directory holding projects from several ecosystems, which a scan records as one project per ecosystem — is refused (exit 2), whichever form you use. fix picks the project to modify by path, so with several projects at one path it cannot tell them apart, and picking one would be arbitrary. Upgrade the affected package with that ecosystem’s own package manager until per-ecosystem targeting lands.

If the scan cache cannot be read, the name reading is announced as unavailable on stderr; it is never silently reported as “no such project”.

What --apply upgrades: with --cve, only the dependency for that advisory; otherwise (--all, or neither --all nor --cve) every previewed fix is applied. In other words, a bare fix <project> --apply upgrades all fixable dependencies — run it once without --apply first to preview the full set.

After each apply the project is re-enriched and the targeted advisory is re-checked: a fix is reported “Fixed — verified” only when the advisory is gone. If it persists (the package manager resolved a different version, or there is no real fix) or the re-check couldn’t run, it reports “applied but NOT confirmed” and the command exits non-zero — a still-vulnerable state never reads as success. The two cases are distinguishable: a completed re-check that still finds the advisory exits 1 (a finding), a re-check that could not run exits 2 (unproven), and a fix that failed to apply at all exits 3.

Terminal window
packageprobe fix /path/to/project # preview, by path
packageprobe fix my-app # preview, by project name
packageprobe fix prj_9f3a1c2b # preview, by stable id
packageprobe fix /path/to/project --cve CVE-2021-23337 --apply
packageprobe fix /path/to/project --all --apply

Print the resolved config path and its contents (as JSON).

Terminal window
packageprobe config

Verify the integrity of the audit-log hash chain.

Terminal window
packageprobe audit-verify [OPTIONS]
FlagTypeDefaultDescription
--path <PATH>path~/.packageprobe/logs/audit.jsonlAudit log to verify.
--include-rotatedflagfalseWalk all rotated log files (audit.jsonl.5 → .1 → active), not just the active file. Required when the chain spans rotation boundaries.
--jsonflagfalseOutput the result as JSON.

A missing or emptied log verifies as no evidence (exit 2), not “valid” — an absent log is never proof of a clean history.

Start a 7-day free trial (all features unlocked).

Terminal window
packageprobe trial <EMAIL>

Activate this machine with a license key.

Terminal window
packageprobe login <KEY> [--email <EMAIL>]
FlagTypeDescription
--email <EMAIL>stringBind this activation to a specific seat on a multi-seat enterprise license. Single-seat licenses ignore it (activation falls back to max_machines enforcement).

Deactivate this machine (frees the seat).

Terminal window
packageprobe logout

Inspect or manage the active license.

Terminal window
packageprobe license status # print current license status as JSON
packageprobe license refresh # re-fetch the license from the server, re-validate,
# pick up pack changes, reset the 7-day rolling TTL

For ephemeral CI runners where per-machine seat activation doesn’t make sense, validate statelessly with the PACKAGEPROBE_LICENSE_KEY environment variable — no machine activation, no default license file. License resolution precedence is: an explicit --license-file <PATH> wins if provided; otherwise PACKAGEPROBE_LICENSE_KEY is checked before the default ~/.packageprobe/license.json. So in CI, set the env var and don’t pass --license-file.

Terminal window
export PACKAGEPROBE_LICENSE_KEY=pp_live_...
packageprobe scan --vulns --policy

Exit codes make this pipeline-friendly: gate a build on 0 (pass), branch on 1 (violation), and treat 2/3 as “don’t trust this result” — a degraded enrichment or an incomplete scan exits non-zero so the step can’t stay green on an incomplete result. The packageprobe/scan-action GitHub Action wraps all of this (install, checksum-verified download, scan, SBOM, policy, and optional PR-comment diffing).