Audit a lockfile you did not write
Somebody sends you a repository. A contractor's handover, a candidate's
take-home, a dependency you are about to vendor, an npm package you are
considering. You want to know what is in the tree before you type npm install,
because npm install is the part that runs other people's code.
$ ./target/release/stranger scan path/to/their/package-lock.json
Nothing is installed, nothing is resolved, no node_modules appears, and the
registry is never contacted. The lockfile is read as text.
Why the order matters
The usual way to inspect a dependency tree is to install it first and then ask the installed tree questions. That is backwards for an unknown repository: the install is the risky operation.
That risk is measurable from the file itself, which is the point of the install scripts rule:
$ ./target/release/stranger scan -v fixtures/npm-m.package-lock.json | head -8
npm-m.package-lock.json 576 packages (20 direct · 556 transitive · 6 workspace)
⚠ INSTALL SCRIPTS 4 arbitrary code at install time
esbuild@0.27.7 runs code at install time · lockfile records the flag, not the script
fsevents@2.3.3 runs code at install time · lockfile records the flag, not the script
sharp@0.34.5 runs code at install time · lockfile records the flag, not the script
unrs-resolver@1.12.2 runs code at install time · lockfile records the flag, not the script
Four packages whose code runs before yours does, named before you install any of them. Reading the lockfile inverts the order: the tool never executes anything from the tree, and it has nothing of its own to execute either — an empty dependency manifest means auditing a hostile lockfile cannot pull a hostile parser.
On a file, not a directory
Point at the whole repository and discovery does the work: it walks six levels
down, skipping node_modules and twelve other named directories, and picks up
any of the seven filenames in lock::KNOWN — package-lock.json,
pnpm-lock.yaml, Cargo.lock, requirements.txt, poetry.lock, uv.lock,
go.mod.
On a handover that is what you want, because you do not yet know which
ecosystems are in there.
Point at a single file and discovery is skipped entirely. The format is then chosen by the same suffix match, so an archived or renamed lockfile still reads:
$ ./target/release/stranger scan fixtures/poisoned.package-lock.json
poisoned.package-lock.json 757 packages (35 direct · 722 transitive)
⚠ HALLUCINATION RISK 3
chalck@5.3.0 not in corpus · d=1 from "chalk" · root-only, no parent
expres@4.18.2 not in corpus · d=1 from "express" · root-only, no parent
lodahs@4.17.21 not in corpus · d=1 from "lodash" · root-only, no parent
⚠ INSTALL SCRIPTS 3 arbitrary code at install time
⚠ TRIVIAL 35 (4.6% of third-party)
⚠ VERSION DRIFT 55 same package at 2+ versions in one tree
· UNPINNED — no signal in this format
risk 81/100 94ms third-party deps used to compute this: 0
One path per run. A second positional argument is a usage error rather than a silently ignored one:
$ ./target/release/stranger scan a/package-lock.json b/package-lock.json
stranger: scan takes one path; got a second, `b/package-lock.json`
Loop in the shell if you have several:
$ for f in */package-lock.json; do ./target/release/stranger scan "$f"; done
Reading the result
The header tells you the size of what you were about to install. 757 packages (35 direct · 722 transitive) means 35 names somebody chose and 722 that came along.
Read the critical block first — it is listed in full because it is the answer.
Then decide whether the counts underneath deserve a -v. On a tree you did not
write, INSTALL SCRIPTS usually does.
A finding tells you a name has no evidence behind it, or that a package runs code, or that something is installed twice. None of that says the package is malicious, and a clean scan does not say the tree is safe. Limits is the honest list of what is not checked, and integrity hashes are at the top of it.
What it will not tell you
Whether the sha512-… integrity fields are correct. Whether an install script is
benign. Whether a package that genuinely exists is nonetheless hostile. Those need
either cryptography or the network, and this binary has neither.
$ ./target/release/stranger scan -v fixtures/npm-xl.package-lock.json | head -14