Skip to main content

licenselens

Find the licenses that will stop you shipping.

PyPI Python License: AGPL-3.0

Most license scanners tell you the name of a license. licenselens tells you whether you can ship, and answers correctly on the cases where naive scanners are wrong: the copyleft license that mentions a harsher one, the permissive file that carries an "All rights reserved" notice, the Creative Commons dataset that is one word away from being uncommercializable.

No runtime dependencies. No API key. No install required.

licenselens auditing five repositories


The three failures this tool exists to prevent

1. GPL-3.0 misread as AGPL-3.0

Section 13 of GPL-3.0 is literally titled "Use with the GNU Affero General Public License". Any scanner that greps a license file for "Affero" concludes AGPL-3.0 — a network copyleft license whose obligations are materially harsher. Teams then either over-comply for no reason or stop trusting the tool.

$ licenselens judge GPL-3.0.txt
GNU General Public License v3.0  [GPL-3.0]
  confidence   99%
  copyleft     project

licenselens scores evidence by region. A signal may be a title or a body mention, and only title evidence can name the license. GPL-3.0's mention of Affero sits 550 lines into the body, so it carries no promotion weight. AGPL-3.0 scores 0 against a GPL-3.0 file.

2. BSD-3-Clause misread as BSD-2-Clause

Clauses 1 and 2 are identical in both licenses. Only clause 3 — "Neither the name of … may be used to endorse" — separates them. Score the shared clauses highly and BSD-2 wins every time. This is not hypothetical: it happened to pallets/flask on this tool before it was fixed, 145 to 140.

The fix is structural. Per-license scores are split into total and distinctive evidence, where distinctive means signals no other profile shares, and ties break on distinctive evidence:

BSD-3-Clause   total=170  distinctive=140   <- wins
BSD-2-Clause   total= 90  distinctive= 60

3. "No LICENSE file" read as "free to use"

A public repository without a LICENSE file is not open source. Default copyright applies: all rights reserved. This is the most common and most expensive mistake in dependency review, and it is invisible to a scanner that only reports license names — it reports nothing, and nothing reads as fine.

licenselens reports it as a blocking finding with a remedy, and separates it from copyleft risk so the two do not get conflated in your ticket queue:

  • license risk — terms forbid or endanger commercial use → buy, replace, or isolate
  • hygiene risk — no license, unknown license, dead project → get permission, pin, or vendor

Install

Nothing to install:

git clone https://github.com/yuehancn/licenselens
cd licenselens
python licenselens.py judge LICENSE

Or install it:

pip install licenselens-cli
licenselens --version

The distribution is named licenselens-cli because licenselens on PyPI belongs to an unrelated project. The command you run is still licenselens.

Python 3.10+. Zero dependencies.

Use

Classify a license file (offline, no network)

licenselens judge LICENSE
licenselens judge LICENSE --quiet        # just the SPDX id
licenselens judge LICENSE --json         # full evidence trail
licenselens judge LICENSE --policy saas  # evaluate against your deployment model
== LICENSE
MIT License  [MIT]
  confidence   100%
  commercial   allowed
  copyleft     none
  obligations
    - Attribution only; keep the copyright notice and license text.
  evidence
    MIT               270
    ISC                 0
  decision     allow
  summary      Shippable as-is under the current policy.

Audit repositories (no API quota consumed)

licenselens audit pallets/flask n8n-io/n8n immich-app/immich
licenselens audit --file repos.txt --format markdown -o audit.md
licenselens audit --file repos.txt --format sarif -o licenses.sarif
STATUS REPOSITORY                               LICENSE                  DECISION         ★
------------------------------------------------------------------------------------------
OK    pallets/flask                            BSD-3-Clause             allow        74934
FAIL  immich-app/immich                        AGPL-3.0                 block       117582
FAIL  n8n-io/n8n                               Sustainable-Use-1.0      block       208992
FAIL  anthropics/skills                        NONE                     block       184680
------------------------------------------------------------------------------------------
4 repos: 1 allow, 0 review, 3 block

Blocking issues
  anthropics/skills
    [NO-LICENSE] No license file: default copyright applies, so all rights are
    reserved and commercial use is not permitted.
    -> Get written permission from the author, or replace the dependency. Do not
    treat a public repository as free to use.

Audit your own dependency tree (offline, or with registry lookup)

licenselens scan .                 # offline: reads what your manifests declare
licenselens scan . --resolve       # also look up undeclared licenses in the registry
STATUS PACKAGE                            LICENSE                    DECISION
----------------------------------------------------------------------------------------
OK    react                              MIT                        allow
OK    express                            MIT                        allow
OK    requests                           Apache-2.0                 allow
OK    django                             BSD-3-Clause               allow
FAIL  demo-service                       AGPL-3.0                   block

Evaluate an SPDX expression

licenselens expr "MIT OR GPL-3.0" "MIT AND AGPL-3.0"

OR means you may pick a branch, so the permissive one wins. AND means every branch applies, so the harshest term wins. Getting these backwards is a common and expensive misreading of package.json.

See the knowledge base

licenselens facts

Exit codes and CI

0 when everything is allowed, 1 when something is blocked, 2 on bad input. Use --fail-on review to fail on anything that needs a human, or --fail-on never to report without failing.

GitHub Actions

name: license
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with: { python-version: "3.12" }
      - run: pip install licenselens-cli
      - name: Audit dependencies
        run: licenselens scan . --resolve --fail-on block
      - name: Audit repository (SARIF to the Security tab)
        run: licenselens audit --file repos.txt --format sarif -o licenses.sarif
        continue-on-error: true
      - uses: github/codeql-action/upload-sarif@v4
        with: { sarif_file: licenses.sarif }

SARIF findings appear inline on the pull request, so a license problem shows up where the code review happens instead of in a report nobody opens.

Why it works on hostile networks

raw.githubusercontent.com is blocked or black-holed on plenty of corporate and regional networks, and GitHub's unauthenticated API allows 60 requests an hour — about 24 repositories. Two design choices address this:

Dual channel. The HTML channel reads repository pages and the license body embedded in the blob payload. It needs no token, consumes no API quota, and does not touch raw.githubusercontent.com. The API channel is used to enrich metadata. Both are available; the HTML channel is the default for bulk work.

Reads escalate through three forms because GitHub uses three: a <textarea>, a "rawLines":[...] array, and "rawLines":null for large files. The third one sank an early version: redis/redis is 72 KB, came back empty, and was reported as unlicensed. Reporting a licensed project as unlicensed is the worst error this tool can make, so each form gets a test and a fallback (github.com/{repo}/raw/, never the blocked raw host).

Circuit breaker. A black-holed host does not fail fast — it hangs until the socket times out. Retrying a filename list against it turns a 24-repo audit into an hour of silence, which is the failure mode that makes people abandon a tool. After two consecutive transport failures the host is tripped and further requests return instantly, so the caller degrades to the other channel in microseconds.

One real 5-repository audit: 20 requests, 0 breaker trips, 0 API quota used.

A note on scraping: GitHub's markup is not a contract. The license body moved from a <textarea> to a "rawLines":[...] React payload, and the star counter id changed, both within the last year. Both shapes are supported, and a parse failure degrades to the API channel rather than reporting "no license" — the specific failure that would silently block every dependency.

Policy, not just facts

A license name is not a decision. --policy selects the commercial posture:

policy weak copyleft (MPL/LGPL) strong copyleft (GPL) network copyleft (AGPL)
commercial (default) allow block block
permissive block block block
saas allow allow block
copyleft allow allow allow

saas is the one worth understanding: GPL is fine when you never distribute the binary, but AGPL closes exactly that loophole. That distinction decides whether a dependency is a legal problem or not.

Your own policy, in the repo

Four postures cannot cover every organisation. Somebody always has one signed-off exception, and the usual outcome is that the gate gets ignored. --policy-file moves that decision into a reviewed file:

# licenselens.policy.yml
extends: commercial
accept:
  AGPL-3.0: review      # signed off, behind a documented process boundary
  LGPL-3.0: allow       # static linking cleared in writing
settings:
  max_staleness_days: 365
  require_patent_grant: true
licenselens policy licenselens.policy.yml       # validate, no scan needed
licenselens scan . --policy-file licenselens.policy.yml

Two behaviours are deliberate:

  • A typo fails the run (exit 2, with the line number). A misspelled key or license id would otherwise parse, change nothing, and leave you certain you had relaxed a rule you had not.
  • An override replaces the licence judgement, not the maintenance one. "We accept AGPL" says nothing about "we accept an unmaintained dependency", so STALE and ARCHIVED findings still apply.

The loader implements a strict YAML subset — no anchors, flow collections or multi-document files. Unsupported constructs are reported by name rather than half-parsed. A full worked example is in examples/policy/acme.policy.yml.

What it does not do

  • It is not legal advice. It is a well-tested reading of license text. Have a lawyer review anything that blocks a release you care about.
  • It does not resolve full transitive trees. It reads declared dependencies and can look them up in a registry; it does not build a complete SBOM from a lockfile across every ecosystem.
  • It does not detect license violations in your code, only the terms of dependencies you name.
  • It does not cover every license in existence. 18 families are modelled, covering the overwhelming majority of real dependency trees. Anything else comes back as UNRECOGNIZED with decisive=False, on purpose.

Test suite

python licenselens.py selftest
82/82 passed

The fixtures are canonical license texts fetched from the GitHub Licenses API and the SPDX license list, committed verbatim. Every assertion corresponds to a real misclassification, including the three above. Notable properties under test:

  • every canonical license is identified, including the GPL/AGPL and BSD-2/BSD-3 traps
  • verdicts are order-independent — permuting the profile list changes nothing (an if/elif chain fails this by construction)
  • every profile has a title-region signal, so none can be hijacked by body mentions
  • a short CC notice is resolved from family plus modifiers, so CC-BY-NC-SA-4.0 needs no profile of its own
  • MIT OR GPL-3.0 allows; MIT AND AGPL-3.0 inherits the network clause
  • all three GitHub license-body markup shapes are parsed, including the lazy null one that silently produced "no license" for a 72 KB file
  • verified against ten high-star repositories and their published licenses: 10/10

If you are wondering whether this thing works

It runs on itself, and the answer is uncomfortable:

$ licenselens audit yuehancn/licenselens
FAIL  yuehancn/licenselens    AGPL-3.0    block
    [NETWORK-COPYLEFT] AGPL-style network clause: serving this to users over a
    network obliges you to offer the complete corresponding source.

That is correct. This project is AGPL-3.0, so a closed-source product that hosts it would owe source — which is precisely why a commercial license exists. A tool that flatters its own author is not measuring anything.

License

Dual-licensed: AGPL-3.0-or-later for open use, or a commercial license for closed-source distribution and hosted services.

You do not need to buy anything to run this in your own company or CI. See COMMERCIAL.md for the exact boundary — it is written to be read in two minutes, and it tells you plainly when the free option is enough.

Contributing

See CONTRIBUTING.md. The short version: a wrong verdict is worse than no verdict, so detection changes need a fixture of the real license text. Reporting a misclassification is the most valuable thing you can send.

Metadata

Release files for licenselens-cli 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for licenselens-cli 0.2.1
File Size Uploaded Provenance Actions
licenselens_cli-0.2.1.tar.gz 83.5 kB Without provenance Details for licenselens_cli-0.2.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for licenselens-cli 0.2.1
File Interpreter ABI Platform Actions
licenselens_cli-0.2.1-py3-none-any.whl Python 3 none any Details for licenselens_cli-0.2.1-py3-none-any.whl

Total release size: 156.1 kB

Release files / licenselens_cli-0.2.1.tar.gz

Download URL licenselens_cli-0.2.1.tar.gz
Size 83.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b4025b96d0c71d81bda247c2881e770a23b6d30a99e8e6fccb2562cb69e2c60c
BLAKE2b-256 checksum
How to use checksums
27d2c4768f7fbd0ec97ebe9b86c1897b748f2780731bf75c7a29902aa34bdd40
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / licenselens_cli-0.2.1-py3-none-any.whl

Download URL licenselens_cli-0.2.1-py3-none-any.whl
Size 72.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1198c1788da1e22b716956585b78da0ae48ad1316b7b3d6cfa9e27f541b925d5
BLAKE2b-256 checksum
How to use checksums
2d5427c89eb5325720d0467cf222a59977d2d8872e04629a833a6bb485025c5c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Security for licenselens-cli 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page