Troubleshooting

"no lockfile in ." on a project that has one

Discovery recurses from the directory you named and matches filenames ending in one of seven known names. A file renamed at the front still reads; one renamed at the back does not.

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

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

The usual cause is a name like requirements-dev.txt, which ends in .txt and not in requirements.txt. Nothing reads file contents to second-guess the name, so pointing straight at it does not help either — that is exit 2, "not a lockfile stranger knows". Copy or symlink it to a name in the list.

The other cause is a lockfile inside node_modules, target, .venv or one of the ten other skipped directories, or deeper than six levels down.

Point at a file to skip the walk:

$ ./target/release/stranger scan fixtures/npm-s.package-lock.json

  npm-s.package-lock.json  405 packages   (12 direct · 393 transitive)

  ⚠  INSTALL SCRIPTS        3     arbitrary code at install time

  ⚠  TRIVIAL                10    (2.5% of third-party)

  ⚠  VERSION DRIFT          30    same package at 2+ versions in one tree

  ·  UNPINNED               — no signal in this format

  risk 56/100    92ms    third-party deps used to compute this: 0

Matching on a file path is by suffix, so npm-s.package-lock.json and old.package-lock.json both read.

Exit code here is 0, not an error. A repository of mixed languages should not turn red on the directories the tool has nothing to say about.

"lockfileVersion 1 is not supported"

$ mkdir -p /tmp/v1
$ echo '{"lockfileVersion": 1, "dependencies": {}}' > /tmp/v1/package-lock.json
$ ./target/release/stranger scan /tmp/v1
stranger: /tmp/v1/package-lock.json: lockfileVersion 1 is not supported; stranger reads 2 and 3. Run `npm install` with npm 7 or newer to upgrade the file.

Do what it says. npm 7 and later write version 2 or 3, which have the packages map this reader needs. Version 1 kept the tree in a nested dependencies object and cannot be read the same way.

"not a lockfile stranger knows"

$ mkdir -p /tmp/renametest
$ printf 'flask==3.0.0\n' > /tmp/renametest/requirements-dev.txt
$ ./target/release/stranger scan /tmp/renametest/requirements-dev.txt
stranger: requirements-dev.txt: not a lockfile stranger knows. It reads: package-lock.json, pnpm-lock.yaml, Cargo.lock, requirements.txt, poetry.lock, uv.lock, go.mod, yarn.lock

Seven formats in this build, and the error lists them from the same constant discovery uses, so the message cannot drift out of date the way this page did.

You asserted the file was a lockfile and it does not match any name, so this is exit 2 rather than the exit 0 a directory scan gives. Renaming it to requirements.txt reads it — the name is the whole test, and nothing looks inside to confirm.

"no packages map"

The file parsed as JSON and had a lockfileVersion of 2 or more, but no packages object. Either it is truncated or it is not a package-lock.

A syntax error with a line and column

$ mkdir -p /tmp/bad
$ echo '{"lockfileVersion":3, "packages" 1}' > /tmp/bad/package-lock.json
$ ./target/release/stranger scan /tmp/bad/package-lock.json
stranger: /tmp/bad/package-lock.json: expected ':' at 1:34

pip errors quote the fragment as well as the position:

$ printf 'flask[async>=3.0\n' > /tmp/bad/requirements.txt
$ ./target/release/stranger scan /tmp/bad/requirements.txt
stranger: /tmp/bad/requirements.txt: `flask[async>=3.0` has an unclosed `[` in its extras at 1:1

Line and column are 1-based and the column counts characters, not bytes, so it lines up with what an editor shows you. A lockfile that fails to parse is usually a merge conflict marker or a truncated download.

Nothing found on a lockfile you expect findings in

Work through the three clauses of the detection rule in order. It fires only when all three hold.

  1. The name is in the corpus. 140,066 npm names and 15,000 PyPI names are compiled in; if the name is one of them the rule stops immediately. This is the case for a typosquat that actually got registered.
  2. No corpus name is within edit distance 2. A hallucinated name that is not a near-miss of a real one has no parent for clause 2 to find. Check the range is really empty before concluding this — requests-http was assumed to be in this category and turned out to be two edits from requests-html.
  3. Something depends on it. If any third-party package in the tree lists the name, the rule goes quiet by design. Edges out of the root manifest and out of workspace members do not count for this; edges out of any other package do. On a requirements.txt there are no edges at all, so this clause never suppresses anything.

Also check the package is not first-party. Workspace directories and link: true entries are skipped by every rule before any clause runs.

Only the critical block is listed

By design. Non-critical rules collapse to a count and a reason, because a 1,390-package tree produces 76 drift findings and 29 trivial ones. -v expands them:

$ ./target/release/stranger scan -v fixtures/npm-xs.package-lock.json

  npm-xs.package-lock.json 37 packages   (1 direct · 36 transitive)

  ⚠  TRIVIAL                4     (10.8% of third-party)
     es-errors@1.3.0          one expression, one publisher · inlining it removes an account from your build
     gopd@1.2.0               one expression, one publisher · inlining it removes an account from your build
     has-symbols@1.1.0        predicate-shaped, resolves nothing · size not measured, see rule docs
     hasown@2.0.4             one expression, one publisher · inlining it removes an account from your build

  ·  UNPINNED               — no signal in this format

  risk 9/100    9ms    third-party deps used to compute this: 0

--format json is never collapsed — it emits every finding whether or not you passed -v.

Findings you believe are wrong

Most likely the package is real and newer than the corpus snapshot of 2026-08-28, or it fell off a popularity ranking. tensorflow-gpu in fixtures/reqs-xs.requirements.txt is exactly that, and it is shipped as a fixture rather than hidden. False positives covers the shape of it and the ablation table puts numbers on it.

The nearest name in detail is the closest corpus entry, which is not always the name you would have guessed, and gets less reliable as the corpus ages. Treat it as the rule showing its working.

For TRIVIAL hits specifically: that rule cannot see how long a file is, and reports a good share of packages that are not one-liners. Trivial packages says which and why.

No colour

Colour is on when stdout is a TTY. Four inputs decide, highest priority first: --no-color, then NO_COLOR, then CLICOLOR_FORCE, then TTY. Off beats on at every tie.

If a CI log renders ANSI and you want colour there, set CLICOLOR_FORCE=1. If you are getting escape codes where you do not want them, --no-color beats everything else. --format json never carries colour under any setting.

A closed pipe prints nothing and exits 0

$ ./target/release/stranger scan fixtures/poisoned.package-lock.json | head -3

  poisoned.package-lock.json 757 packages   (35 direct · 722 transitive)

$ echo $?
0

EPIPE is the shell working correctly, not a failure, so it is silent. If a piped run reports an error, the error is real and came from somewhere else.

"scan takes one path"

$ ./target/release/stranger scan a/package-lock.json b/package-lock.json
stranger: scan takes one path; got a second, `b/package-lock.json`

One path per run, on purpose — a second argument being silently ignored is worse. Loop in the shell. A directory containing both a package-lock.json and a requirements.txt is one path and scans both.

$ ./target/release/stranger scan -v fixtures/poisoned.requirements.txt