How to check npm package licenses

Run npx license-checker --summary in the project root. It prints one line per distinct license value with a count. This repository reported 253 packages across 12 values, three of which are not plain identifiers: two SPDX expressions and one UNLICENSED. Read those three by hand.

Why check this

License review happens before a release that ships compiled output, and again whenever a dependency upgrade adds names to the tree. The whole check is a list of strings, which is why it is usually automated badly.

The failure it prevents is a copyleft term arriving inside an expression. @img/sharp-wasm32 here is Apache-2.0 AND LGPL-3.0-or-later AND MIT. A reviewer scanning a summary sees the familiar words at both ends and moves on, and the allowlist step below waves the package through even when LGPL is not on the list.

Prerequisites

{
  "name": "licdemo", "version": "1.0.0", "private": true, "license": "MIT",
  "dependencies": {
    "dual-licensed": "file:vendor/dual-licensed",
    "either": "file:vendor/either",
    "no-license": "file:vendor/no-license"
  }
}
{ "name": "dual-licensed", "version": "1.0.0", "license": "Apache-2.0 AND LGPL-3.0-or-later" }
{ "name": "either", "version": "1.0.0", "license": "(MIT OR GPL-3.0-only)" }
{ "name": "no-license", "version": "1.0.0" }

Put each vendored manifest in vendor/<name>/package.json and run npm install.

Steps

  1. Step 1.

    Count the license values in the project you care about.

    npx --yes license-checker@25.0.1 --summary 2>/dev/null
    
    ├─ MIT: 212
    ├─ ISC: 11
    ├─ Apache-2.0: 10
    ├─ BSD-3-Clause: 5
    ├─ MPL-2.0: 5
    ├─ BlueOak-1.0.0: 4
    ├─ Apache-2.0 AND LGPL-3.0-or-later AND MIT: 1
    ├─ Apache-2.0 AND LGPL-3.0-or-later: 1
    ├─ CC-BY-4.0: 1
    ├─ BSD-2-Clause: 1
    ├─ UNLICENSED: 1
    └─ 0BSD: 1

    Twelve values over 253 packages. Two of the twelve are SPDX expressions rather than identifiers, and one is the placeholder npm uses for a package that publishes no license.

  2. Step 2.

    Pull out every value that is not a single identifier, and name the package behind it.

    npx --yes license-checker@25.0.1 --json 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{for(const [k,v] of Object.entries(JSON.parse(s))){const L=String(v.licenses);if(/ AND | OR |UNKNOWN|UNLICENSED/.test(L))console.log(L+'  '+k);}})"
    
    Apache-2.0 AND LGPL-3.0-or-later AND MIT  @img/sharp-wasm32@0.35.4
    Apache-2.0 AND LGPL-3.0-or-later  @img/sharp-win32-x64@0.35.4
    UNLICENSED  h2check@0.1.0

    Three rows out of 253. The last one is the repository itself, which the report includes alongside its dependencies.

  3. Step 3.

    Switch to the fixture and read the four shapes a license field can take.

    npx --yes license-checker@25.0.1 2>/dev/null | grep -E "^├─|^└─|licenses:"
    
    ├─ dual-licensed@1.0.0
    │  ├─ licenses: Apache-2.0 AND LGPL-3.0-or-later
    ├─ either@1.0.0
    │  ├─ licenses: (MIT OR GPL-3.0-only)
    ├─ licdemo@1.0.0
    │  ├─ licenses: UNLICENSED
    └─ no-license@1.0.0
     ├─ licenses: UNKNOWN

    no-license has no license field at all and becomes UNKNOWN. licdemo declares "license": "MIT" and is reported as UNLICENSED, because its manifest also sets "private": true.

  4. Step 4.

    Test the allowlist flag against the AND expression, one term at a time.

    for a in "Apache-2.0" "LGPL-3.0-or-later" "MIT"; do npx --yes license-checker@25.0.1 --onlyAllow "$a" --excludePackages "licdemo@1.0.0;no-license@1.0.0;either@1.0.0" >/dev/null 2>&1; echo "$a -> exit $?"; done
    
    Apache-2.0 -> exit 0
    LGPL-3.0-or-later -> exit 0
    MIT -> exit 1

    dual-licensed is Apache-2.0 AND LGPL-3.0-or-later. Allowing either term on its own lets it pass. The flag reads AND the way it reads OR.

  5. Step 5.

    Repeat the same test on the real tree, with a list an audit would plausibly use.

    npx --yes license-checker@25.0.1 --onlyAllow "MIT;ISC;Apache-2.0;BSD-2-Clause;BSD-3-Clause;0BSD;BlueOak-1.0.0;CC-BY-4.0;MPL-2.0;UNLICENSED" >/dev/null 2>&1; echo "LGPL-3.0-or-later not on the list: exit $?"
    
    LGPL-3.0-or-later not on the list: exit 0

    The list permits ten identifiers and LGPL is not among them. Two packages carrying it passed. Dropping MPL-2.0 from the same list did produce exit 1, naming lightningcss-win32-x64-msvc@1.30.1, so the flag works on plain identifiers.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | A plain identifier such as MIT | One SPDX license, matched by string comparison | Compare it against your list and move on. | | A AND B | Both sets of terms apply to the package | Review every term. --onlyAllow passes it on any one of them. | | (A OR B) | The package offers a choice and you take one | Record which one you chose, because the report will not. | | UNKNOWN | The manifest has no license field | Open the package directory and look for a LICENSE file before assuming anything. | | UNLICENSED | The manifest sets private: true, or declares that value | Exclude your own packages with --excludePrivatePackages. |

Common mistakes

Sign: An allowlist run passes, and a package in the tree carries a license the list does not permit.Cause: --onlyAllow splits an SPDX expression and accepts the package when any single term is allowed. A package licensed Apache-2.0 AND LGPL-3.0-or-later passed a list holding only Apache-2.0, and passed again on a list holding only LGPL-3.0-or-later. The README describes the flag as a list of permitted licenses and says nothing about expressions.
Sign: Your own package appears in the report as UNLICENSED although its manifest names a license.Cause: private: true wins. The fixture root declares MIT and is reported as UNLICENSED. The report also includes the root project next to its dependencies, so a count of report rows is one higher than a count of dependencies.
Sign: A script compares the license string against an array and flags packages nobody objects to.Cause: Exact matching cannot read (MIT OR GPL-3.0-only), which is a package you may take under MIT. It also cannot read UNKNOWN, which means an absent field rather than a permissive one. Parse the expression, or resolve those rows by hand and record the decision.

What to check next

FAQ

How do I check npm package licenses?

npx license-checker --summary for the counts, then --json for the rows behind any value you do not recognise. Run it after an install, because it reads node_modules rather than the registry.

What does UNKNOWN mean in the report?

The package manifest has no license field. It is not a statement that the code is free to use. Open the package directory and look for a LICENSE file, then ask the publisher if there is none.

Can I fail a build on a forbidden license?

--onlyAllow returns exit 1 on a plain identifier that is not on the list. It returns 0 for an SPDX expression whose terms are only partly allowed, so pair it with a pass over the JSON output.

Why is my own project in the list?

The root package is reported with its dependencies. --excludePrivatePackages removed it from the fixture report here, and --excludePackages "name@version" removes any named package.

Verified

Verified by Maks Vernylicense-checker 25.0.1npm 10.9.8Node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate8 minpublished updated Maks Verny