A few months ago a lawyer asked our team a simple question: "Can you prove
nothing in this product is GPL?" We couldn't — not quickly. A couple thousand
transitive deps across Node and Python services, and the honest answer was "uh,
probably?", which is not what you want to tell a lawyer.
So I went looking for a tool to just tell me, locally, right now, which
licenses my dependencies carry and which ones are a problem. What I found:
-
license-checker— the npm default, ~900K weekly downloads — has been unmaintained for years, and it dumps raw license strings; it won't tell you GPL is a bigger deal than MPL. - Snyk, FOSSA, Black Duck — all good, all want a signup, an API token, and a network round-trip before classifying a folder already on my disk.
But the info I needed was already in my node_modules: every package ships a
package.json license field, every Python wheel a METADATA file. Why am I
uploading anything?
So I built licsniff — a zero-dependency CLI that reads those files locally,
classifies each license into a risk tier, and exits. No account, no network,
nothing to set up.
npx licsniff
PACKAGE VERSION LICENSE RISK
some-gpl-lib 2.1.0 GPL-3.0 strong-copyleft
mystery-pkg 0.0.3 (none) unknown
copyleft-utils 1.4.0 LGPL-2.1 weak-copyleft
left-pad 1.3.0 MIT permissive
fast-json 3.1.4 (MIT OR Apache-2.0) permissive
Riskiest first. The line you actually need to worry about is at the top.
Tiers, not just strings
The whole point is that "GPL-3.0" is only useful if you know what bucket it falls
into. licsniff sorts every license into one of five tiers:
- permissive — MIT, ISC, BSD, Apache-2.0, 0BSD, Unlicense, CC0… use freely.
- weak-copyleft — LGPL-*, MPL-2.0, EPL-*, CDDL-*. File/linking obligations.
- strong-copyleft — GPL-*, AGPL-*. Can force you to open-source your code.
-
proprietary —
UNLICENSED,SEE LICENSE IN …. Not open source at all. - unknown — missing or unrecognized. The scariest, honestly — you don't even know what you're shipping.
SPDX expressions, parsed properly
Real metadata isn't clean — you get (MIT OR Apache-2.0), GPL-3.0 AND MIT,
GPLv3, GPL-3.0+, GPL-3.0-only, Apache License 2.0. licsniff normalizes
all of it and evaluates the boolean expressions the way they actually work:
-
OR→ the least restrictive option wins (you pick the friendly one), so(MIT OR GPL-3.0)ispermissive. -
AND→ the most restrictive wins, soGPL-3.0 AND MITisstrong-copyleft. (You don't want a false "permissive" gating your CI here.)
The flag that earns its keep: --fail-on
This is what made it stick on our team. Drop one line in CI:
licsniff --fail-on strong-copyleft
It exits 1 the moment any dependency lands at or above that tier, so a GPL
transitive dep can never sneak in through an npm install again. There's also
--summary for counts and --json | jq for everything else.
It runs on both ecosystems
Half our services are Node, half Python, so licsniff ships on both registries.
Same tool, same tiers; each version audits its own ecosystem:
npx licsniff # Node — scans node_modules, zero deps
pipx run licsniff # Python — scans site-packages, pure stdlib
The Python build reads *.dist-info/METADATA, including the modern PEP 639
License-Expression: field newer wheels use. Both ports share the exact same
classifier, tested against the same vectors, so they tier a license
byte-for-byte identically.
A few design notes
-
One pure function at the core.
classifyLicense(idOrName) → {tier, spdx}has no I/O, no clock, no globals; the CLI is just a thin folder-reader around it. That's why the Node and Python builds can be proven identical — they run one shared test table. - Offline and read-only by design. Never writes a file, never opens a socket. Safe in air-gapped CI, on a client's machine, everywhere.
Try it / break it
Code, issues, and the full README:
It's MIT and small. I'd genuinely like to know which license string it
mis-tiers — paste me a weird one from your node_modules and I'll add it to the
vectors.
How are you checking your dependency licenses today — or are you, like past me,
just hoping no lawyer asks?
Top comments (3)
Offline license checks are underrated because they make the safe path cheap. If a developer has to wait for a separate compliance process, the check will happen too late or not at all.
A tiny CLI that can run locally or in CI is the right shape for this. The ideal output is not just “bad dependency found,” but where it entered, what license triggered it, and what replacement or escalation path exists.
Thanks for the great feedback, Alex!
You hit the nail on the head regarding the "ideal output." Right now, licsniff only handles the "find and classify" part, but revealing the exact dependency path (like showing the ancestors in the dependency tree) and suggesting alternatives is absolutely key to solving the real pain point.
That makes perfect sense as a core feature for our next major release. I'm going to add it to our issue tracker right away to map it out!
Exactly. The license label alone tells you there is risk; the dependency path tells you where to act.
If the tool can show "this package enters through X, used by Y," then replacement becomes a concrete engineering decision instead of a legal panic.