Skip to content

JSON output reference

Every command supports --json: a single JSON document on stdout, exit code unchanged. This page is the machine-consumability contract.

--json and --sarif outputs are stable and snapshot-tested. Changes are additive only — new fields may appear; existing fields keep their names, types, and meanings. Build tooling on them.

Conventions shared by all commands:

  • The document always carries "command": "<name>" and (except audit/scan, where the exit code is threshold-derived from the findings) an explicit "exitCode" field.
  • warnings is always an array of human-readable strings for non-fatal conditions (e.g. "1 package(s) not present in node_modules — run install for full coverage").
  • Severity values are "none" | "low" | "med" | "high" | "critical"; grades are "A" | "B" | "C" | "D" | "F". See Scoring.
  • Exit code 2 (execution error) prints an error to stderr, not a JSON document — treat a non-parseable stdout plus exit 2 as “the run itself failed”.
{
"command": "check",
"dirs": [
{
"dir": "/work/app",
"lockfile": { "path": "/work/app/package-lock.json", "type": "npm" },
"warnings": [],
"queries": [
{
"query": "evil-pkg@1.2.3",
"hit": true,
"matches": [
{
"name": "evil-pkg",
"version": "1.2.3",
"devOnly": false,
"truncated": false,
"paths": [
["<root>", "app-lib@1.0.0", "evil-pkg@1.2.3"],
["<root>", "other-lib@2.0.0", "nested-lib@3.0.1", "evil-pkg@1.2.3"]
]
}
]
}
]
}
],
"hit": true,
"exitCode": 1
}
FieldTypeMeaning
command"check"Command discriminator
incidentobject?Present with --incident: { id, name, date } of the matched bundle
dirs[]arrayOne entry per --dir (or the current directory)
dirs[].dirstringAbsolute project directory
dirs[].lockfile.pathstringAbsolute lockfile path
dirs[].lockfile.type"npm" | "yarn-classic" | "yarn-berry" | "pnpm" | "bun"Detected lockfile format
dirs[].warnings[]string[]Non-fatal notes for this directory
dirs[].queries[]arrayOne entry per query (with --incident, one per bundled package version)
queries[].querystringThe query as resolved, e.g. "evil-pkg@1.2.3"
queries[].hitbooleanDid anything in the resolved tree match
queries[].matches[]arrayOne entry per matching resolved name@version
matches[].name / .versionstringThe matched package
matches[].devOnlybooleanTrue when every path to it goes through dev dependencies only
matches[].truncatedbooleanTrue when path enumeration hit the 500-path cap
matches[].paths[][]string[][]Every dependency path, root-first; "<root>" is your project
hitbooleanAny hit in any directory
exitCode0 | 1Mirrors the process exit code

With --history the document instead carries a history object:

{
"command": "check",
"history": {
"query": "evil-pkg",
"lockfile": "package-lock.json",
"commitsExamined": 3,
"windows": [
{
"version": "1.2.3",
"firstSeen": { "sha": "1f9292d2…", "date": "2026-07-05T16:24:20+05:30" },
"lastSeen": { "sha": "1f9292d2…", "date": "2026-07-05T16:24:20+05:30" },
"stillPresent": false
}
]
},
"hit": true,
"exitCode": 1
}
FieldTypeMeaning
history.querystringThe queried package
history.lockfilestringLockfile path relative to the repo
history.commitsExaminednumberLockfile-touching commits reparsed
history.windows[]arrayOne exposure window per resolved version
windows[].versionstringThe resolved version
windows[].firstSeen / .lastSeen{ sha, date }Full commit sha + ISO-8601 author date bounding the window
windows[].stillPresentbooleanTrue when the current lockfile still resolves this version
{
"command": "audit",
"mode": "diff",
"lockfile": { "path": "/work/app/package-lock.json", "type": "npm" },
"packages": [
{
"name": "dep-a",
"version": "1.0.1",
"key": "dep-a@1.0.1",
"grade": "F",
"findings": [ /* see Finding below */ ]
}
],
"rollup": {
"grade": "F",
"packagesAnalyzed": 2,
"packagesFlagged": 2,
"counts": { "none": 0, "low": 0, "med": 2, "high": 0, "critical": 2 }
},
"warnings": []
}
FieldTypeMeaning
command"audit"Command discriminator
mode"absolute" | "diff" | "deep"Which audit mode produced the report
lockfile.path / .typestringAs in check
packages[]arrayOnly packages with ≥1 finding, worst grade first
packages[].keystring"name@version"
packages[].gradeGradePer-package grade — how grades derive
packages[].findings[]Finding[]Sorted by severity, highest first
rollup.gradeGradeWorst package grade in the tree (A when nothing flagged)
rollup.packagesAnalyzednumberTotal resolved packages
rollup.packagesFlaggednumberPackages with ≥1 finding
rollup.countsobjectFinding count per severity
warnings[]string[]Non-fatal notes
advisories.osvGeneratedAtstringDate the vendored OSV snapshot was generated (YYYY-MM-DD) — the --max-advisory-age basis
advisories.newestIncidentstringDate of the newest vendored incident bundle — event-dated context, not a staleness signal

Baseline fields (additive, only when a baseline is applied)

Section titled “Baseline fields (additive, only when a baseline is applied)”

When a baseline file suppresses findings, three optional fields appear — absent otherwise, per the stability guarantee:

{
"packages": [
{
"name": "with-post",
"version": "1.0.0",
"key": "with-post@1.0.0",
"grade": "A",
"findings": [],
"suppressed": [
{
"layer": 1,
"signal": { /* Finding.signal, unchanged */ },
"severity": "med",
"suppression": {
"reason": "postinstall reviewed — writes a local marker file only",
"addedAt": "2026-07-05"
}
}
]
}
],
"rollup": {
"suppressedCounts": { "none": 0, "low": 0, "med": 1, "high": 0, "critical": 0 }
},
"baseline": {
"path": "/work/app/.lockwarden-baseline.json",
"entries": 2,
"matched": 1,
"expired": 1
}
}
FieldTypeMeaning
packages[].suppressed[]array?Baseline-suppressed findings: a Finding plus a suppression object
packages[].suppressed[].suppressionobjectreason?, addedAt?, expires? copied from the matching baseline entry
packages[].gradeGradeRe-derived from active findings only (F never improves)
rollup.suppressedCountsobject?Suppressed-finding count per severity (active counts stay in rollup.counts)
baseline.pathstringBaseline file that was applied
baseline.entriesnumberTotal entries in the file
baseline.matchednumberFindings suppressed this run
baseline.expirednumberEntries ignored because their expires date passed

Exit codes and --threshold evaluate active findings only — that is the point of a baseline. Layer-2, critical, and grade-F delta findings are never suppressible.

A finding is either Layer 1 (structural signal + corpus-gated severity) or Layer 2 (known-bad match, always critical). Discriminate on layer:

{
"layer": 1,
"signal": {
"analyzer": "lifecycle-scripts",
"code": "LW001D-LIFECYCLE-INTRODUCED",
"kind": "delta",
"package": { "name": "dep-a", "version": "1.0.1" },
"evidence": {
"file": "package.json",
"excerpt": "\"postinstall\": \"node install.js\"",
"detail": "lifecycle script \"postinstall\" is NEW in 1.0.1 (absent in 1.0.0)"
},
"metrics": { "introduced": 1, "changed": 0 }
},
"severity": "critical"
}
FieldTypeMeaning
layer1Layer-1 (structural) finding
severitySeverityCorpus-gated weight for (analyzer, kind)
signal.analyzerstringOne of lifecycle-scripts, binding-gyp, agent-hooks, ide-tasks, size-delta, dep-introduction, obfuscation, phantom-deps, native-binary
signal.codestringStable rule id, e.g. LW001-LIFECYCLE; a D suffix on the number (LW001D-…) marks delta codes
signal.kind"absolute" | "delta"Surface exists vs surface newly appeared this version
signal.package{ name, version }The package the signal is about
signal.evidence.filestring?File inside the package that triggered the signal
signal.evidence.excerptstring?Short quoted evidence
signal.evidence.detailstringHuman-readable explanation
signal.metricsobjectRaw numeric facts (analyzer-specific; what corpus calibration tunes against)
{
"layer": 2,
"severity": "critical",
"code": "LW2-IOC-axios-mar26",
"package": { "name": "plain-crypto-js", "version": "1.0.0" },
"layer2": {
"source": "incident",
"id": "axios-mar26",
"summary": "Phantom transitive dependency plain-crypto-js ran a postinstall payload…"
}
}
FieldTypeMeaning
layer2Known-bad overlay finding — always critical
codestringLW2-OSV-<id>, LW2-IOC-<id>, or LW2-IOC-<id>-FILE (file-content IOC match in scan)
layer2.source"osv" | "incident"Vendored OSV snapshot vs incident bundle
layer2.idstringOSV id (MAL-2026-…) or incident id (axios-mar26)
layer2.summarystringHuman-readable description from the source
{
"command": "drift",
"base": "main",
"lockfile": { "path": "/work/app/package-lock.json", "type": "npm" },
"findings": [
{
"kind": "integrity-swap",
"severity": "critical",
"package": "nested-lib@3.0.1",
"detail": "integrity hash changed for unchanged version nested-lib@3.0.1",
"evidence": {
"baseIntegrity": "sha512-N1yz…",
"currentIntegrity": "sha512-TAMP…"
}
}
],
"warnings": [],
"exitCode": 1
}
FieldTypeMeaning
basestringThe git ref compared against
findings[].kindstringintegrity-swap | unexplained-version | resolved-url-move | patch-introduced-dep — see drift
findings[].severitySeverityFixed per kind (integrity-swap is critical; URL host moves are high)
findings[].packagestring"name@version" the anomaly is about
findings[].detailstringHuman-readable explanation
findings[].evidenceobjectKind-specific before/after values (baseIntegrity/currentIntegrity, baseResolved/currentResolved, bumps, …)
exitCode0 | 1Mirrors the process exit code

Identical to the audit report except:

DifferenceMeaning
"command": "scan"Discriminator
artifact replaces lockfile{ "path": "app.tgz", "kind": "tgz" | "zip" | "dir" | "docker-save", "roots": 2 }roots counts embedded package roots found
packages[].rootPath of the package root inside the artifact, e.g. "package/node_modules/evil-thing"
No mode fieldscan is always absolute analysis + Layer-2 overlay
advisoriesSame freshness stamps as audit (osvGeneratedAt, newestIncident) — dates only, ages never appear in JSON
Baseline fieldsSame as audit: packages[].suppressed, rollup.suppressedCounts, and top-level baseline appear when a baseline is applied
{
"command": "incidents",
"incidents": [
{
"id": "node-ipc-may26",
"name": "node-ipc binding.gyp compromise (May 2026)",
"date": "2026-05-12",
"summary": "Malicious payload delivered via a binding.gyp node-gyp hook…",
"packages": 1,
"fileIocs": 0,
"references": ["https://lockwarden.dev/incidents/"]
}
],
"osv": { "generatedAt": "2026-07-07", "source": "osv.dev npm MAL", "windowMonths": 6, "entries": 5601 },
"exitCode": 0
}
FieldTypeMeaning
command"incidents"Command discriminator
incidents[]arrayEvery bundle this build knows, newest first
incidents[].idstringThe check --incident <id> id
incidents[].name / .date / .summarystringBundle metadata (date is the incident’s event date)
incidents[].packagesnumberPackage entries in the bundle
incidents[].fileIocsnumberFile-content sha256 IOCs (matched by scan)
incidents[].references[]string[]?Source links, when the bundle carries them
incidents[].localtrue?Present only for LOCKWARDEN_INCIDENT_DIR overlays not shipped in this build
osv.generatedAtstringVendored OSV snapshot date — the --max-advisory-age basis
osv.source / .windowMonths / .entriesmixedSnapshot provenance, window (null for seed data), entry count
exitCode0Always 0 — the listing is informational
{
"command": "explain",
"query": "LW001",
"entries": [
{
"id": "LW001",
"name": "lifecycle install script",
"layer": 1,
"analyzer": "lifecycle-scripts",
"codes": { "absolute": "LW001-LIFECYCLE", "delta": "LW001D-LIFECYCLE-INTRODUCED" },
"weights": { "absolute": "med", "delta": "critical" },
"detects": "", "whyItMatters": "", "whatToDo": ""
}
],
"exitCode": 0
}
FieldTypeMeaning
command"explain"Command discriminator
querystring?The queried code — absent when listing all
entries[]arrayOne per family (all of them when no query)
entries[].idstringFamily id (LW001LW009, LW2-OSV, LW2-IOC)
entries[].layer1 | 2Structural analyzer vs known-bad overlay
entries[].analyzerstring?Analyzer id (layer 1 only)
entries[].codesobjectabsolute? / delta? full codes, or pattern? (layer 2)
entries[].weightsobject?{ absolute, delta } severities, read live from the locked table (layer 1 only)
entries[].alwaysCriticaltrue?Layer 2 only — any hit is critical
entries[].detects / .whyItMatters / .whatToDostringThe explanation
entries[].elevationstring?Corpus-tuned compound elevation, when one involves this analyzer
entries[].matchedobject?For a full Layer-2 code: { source: "osv" | "incident", id, summary, packages[] } from the vendored data
exitCode0Always 0 — unknown codes are exit 2 with no JSON document
{
"command": "secrets",
"scanned": { "files": 6, "packages": 2 },
"findings": [
{
"file": "src/config.js",
"line": 6,
"ruleId": "aws-access-key",
"ruleName": "AWS access key ID",
"severity": "high",
"excerpt": "AKIA…LE"
}
],
"warnings": [],
"exitCode": 1
}
FieldTypeMeaning
scanned.filesnumberProject files scanned
scanned.packagesnumbernode_modules packages whose install-path files were scanned
findings[].filestringFile path relative to the project dir
findings[].linenumber1-based line number
findings[].ruleId / .ruleNamestringWhich of the 15 rules matched
findings[].severitySeverityFixed per rule
findings[].excerptstringMasked match — first/last characters only, never the full secret
exitCode0 | 1Mirrors the process exit code

audit and scan emit SARIF 2.1.0 ($schema: https://json.schemastore.org/sarif-2.1.0.json) for GitHub code scanning:

  • tool.driver: name lockwarden, the CLI version, and one rules[] entry per distinct rule id encountered.
  • One results[] entry per finding: ruleId is the signal code (LW001-LIFECYCLE, LW2-IOC-…), level maps Critical→error, High→warning, Med→note, and Low is omitted unless --verbose.
  • locations[].physicalLocation.artifactLocation.uri points at the lockfile (audit) or artifact; logicalLocations[].fullyQualifiedName is the name@version.
  • partialFingerprints["lockwarden/v1"] is a stable hash so GitHub tracks a finding across runs instead of re-opening it each push.
  • Baseline-suppressed findings are still emitted, with a suppressions: [{ "kind": "external", "justification": "<reason>" }] property — GitHub code scanning shows them as suppressed instead of open.

Upload with github/codeql-action/upload-sarif — wired automatically by the GitHub Action. SARIF upload is GitHub-specific; on other platforms consume --json.

Extract every hit path from a check (one -joined line per path):

Terminal window
lockwarden check evil-pkg --json \
| jq -r '.dirs[].queries[] | select(.hit) | .matches[].paths[] | join(" → ")'

Count criticals in an audit:

Terminal window
lockwarden audit --diff main --json | jq '.rollup.counts.critical'

List every package graded D or worse, with its worst finding code:

Terminal window
lockwarden audit --json | jq -r '
.packages[] | select(.grade == "D" or .grade == "F")
| "\(.key)\t\(.grade)\t\(.findings[0].signal.code // .findings[0].code)"'

Fail a script only on delta findings (ignore absolute inventory):

Terminal window
lockwarden audit --diff main --json \
| jq -e '[.packages[].findings[] | select(.signal?.kind == "delta")] | length == 0'

Which dependencies actually need install scripts (the ignore-scripts allowlist):

Terminal window
lockwarden audit --json \
| jq -r '.packages[] | select(any(.findings[]; .signal?.code == "LW001-LIFECYCLE")) | .key'

Exposure windows as CSV:

Terminal window
lockwarden check evil-pkg --history --json \
| jq -r '.history.windows[] | [.version, .firstSeen.date, .lastSeen.date, .stillPresent] | @csv'