stranger

A model suggests a package. You paste the name into package.json. The name is not real — nobody has ever published it — and the first person to notice registers it and ships whatever they like to everyone who runs npm install.

stranger reads a lockfile and tells you which names in it look invented, which of them run code when you install them, and which are installed at four different versions at once. It does not install anything, does not resolve anything, and never opens a socket.

$ ./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    21ms    third-party deps used to compute this: 0

Three planted names, three findings, no false positives. The critical rule lists every hit; the other three collapse to a count until you ask for more with -v.

The empty manifest

The last field of that output is checkable. Cargo.toml has three dependency tables and all three are empty, so the lockfile of a lockfile auditor is one package long:

$ cargo tree
stranger v0.1.0 (/home/keir/stranger)

$ grep -c '^\[\[package\]\]' Cargo.lock
1

That is the whole dependency graph. No serde_json, no clap, no strsim, no toml, no owo-colors. The JSON reader, the TOML reader, the argument parser, the edit distance, the semver comparator, the terminal handling and the report writer are all in src/, and the test harness is the one Rust ships with, so there is no dev-dependency escape hatch in use either. STDLIB.md names each crate that was replaced and what was given up.

#![forbid(unsafe_code)] sits at the top of both src/lib.rs and src/main.rs, and CI fails the build if cargo tree ever prints a second line, if unsafe appears in src/, or if anything reaches for Command::new.

Rust's standard library has no TLS and no HTTP client. A binary with no dependencies therefore cannot make a network request — the guarantee comes from the empty manifest, not from anyone's restraint.

Reading files other tools produced

The hackathon rules forbid shelling out to installed tooling. Their FAQ rules this design in explicitly:

Parsing files those tools already produced is fine, because nothing third-party ends up in your artifact.

Two conditions attach. It has to be disclosed, and it has to degrade when the file is not there. Both hold. The corpus of known-real names and the fixture lockfiles are written up in corpus/PROVENANCE.md, fixtures/README.md and STDLIB.md, and pointing stranger at a directory with no lockfile prints what it looked for and exits 0:

$ rm -rf /tmp/empty && mkdir -p /tmp/empty
$ ./target/release/stranger scan /tmp/empty

  no lockfile stranger reads in /tmp/empty
  looked for: package-lock.json, pnpm-lock.yaml, Cargo.lock, requirements.txt, poetry.lock, uv.lock, go.mod, yarn.lock

$ echo $?
0

What it reads today

Seven formats across four ecosystems: package-lock.json (v2, v3), pnpm-lock.yaml (v9), Cargo.lock (v3, v4), poetry.lock, uv.lock, requirements.txt and go.mod. Five rules, one of which has an idea in it — the co-occurrence rule, which separates a hallucinated name from a legitimate sibling using something other than spelling. The rest of this book is what those things do, what they measure, and where they are wrong.

Start here:

$ make && ./target/release/stranger scan fixtures/poisoned.package-lock.json