permits collects the full license text of every dependency your project
resolves. Instead of trusting the license field in a manifest, it fetches the
actual LICENSE and NOTICE files from wherever the package is published and
works out the SPDX identifier from the file contents.
It supports npm and Go. Almost all of the behavior lives in the library; the CLI is a small wrapper around it.
Every file is stored exactly as published. If the text doesn't match a known
license it's still saved, just without an SPDX id. Detection uses
google/licensecheck and runs on the
file contents, so one file can produce several ids (a combined MIT/Apache-2.0
LICENSE, for example).
permits also recognizes the source-available licenses licensecheck doesn't
cover: FSL-1.1-MIT, FSL-1.1-ALv2, BUSL-1.1, and Elastic-2.0. For an FSL
file it reports the current grant, not the future license embedded in the text.
It looks on disk before going to the network:
| Ecosystem | Scanned file | License source |
|---|---|---|
| npm | pnpm-lock.yaml |
local node_modules, then the npm registry tarball |
| Go | go.sum |
local module cache, then the Go module proxy |
If a package is already installed (npm in node_modules, including pnpm's
.pnpm store, or Go in the module cache), that copy is used and the network is
skipped. A package that exists locally but has no license file is taken at face
value: permits records "no license" rather than falling back to the registry.
By default it scans the whole dependency graph, transitive dependencies
included. -direct limits it to your direct dependencies: the pnpm
importers/root deps, and go.mod requires not marked // indirect.
make build # produces ./permits
# or
go build -o permits ./cmd/permitspermits \
-pnpm-lock ./pnpm-lock.yaml \
-go-sum ./go.sum \
-out ./licenses| Flag | Default | Meaning |
|---|---|---|
-pnpm-lock |
— | path to a pnpm-lock.yaml (repeatable) |
-go-sum |
— | path to a go.sum (repeatable) |
-out |
./licenses |
output directory |
-concurrency |
8 |
parallel fetch workers |
-goproxy |
env GOPROXY | override Go proxy list |
-npm-registry |
npmjs.org | override npm registry base URL |
-node-modules |
lock sibling | node_modules root checked before the registry (repeatable) |
-timeout |
30s |
per-request timeout |
-direct |
false |
only resolve direct (top-level) deps, excluding transitive |
-strict |
false |
exit non-zero if any dependency yields no license |
-v |
false |
verbose progress logging |
Exit codes: 0 on success, 1 if a dependency failed (or -strict and
something had no license), 2 on a usage or I/O error.
permits writes a summary.json and one Markdown file per license file:
licenses/
summary.json
npm/@monetr/notify/1.0.4/EPL-2.0.md
npm/react/19.2.6/MIT.md
go/golang.org/x/mod/v0.17.0/BSD-3-Clause.md
go/gopkg.in/yaml.v3/v3.0.1/Apache-2.0.md # NOTICE -> single SPDX
go/gopkg.in/yaml.v3/v3.0.1/LICENSE.md # dual MIT/Apache -> original name
Paths are <ecosystem>/<name>/<version>/<file>.md. Scoped npm names
(@scope/pkg) and Go module paths (host.com/x/y) become nested directories.
The filename is the SPDX id when exactly one is detected; otherwise it's the
original in-package filename, so dual-licensed or unrecognized files keep their
LICENSE.md or NOTICE.md. Repeated names get a -2, -3 suffix, and .,
.., or path separators inside a segment become _.
Each file is YAML frontmatter followed by the original license text:
---
name: react
version: 18.2.0
ecosystem: npm
declaredLicense: "MIT"
spdx: ["MIT"]
licenseFile: "LICENSE"
source: npm-tarball
sha256: <hex>
retrievedAt: 2026-05-18T00:00:00Z
---
<verbatim license text, emitted exactly as published>
The CLI just calls the library:
import (
permits "github.com/monetr/permits"
"github.com/monetr/permits/output"
)
opts := permits.Options{Concurrency: 8}
c := permits.NewCollector(permits.DefaultRegistry(opts), opts)
summary, artifacts, err := c.Collect(ctx, "pnpm-lock.yaml", "go.sum")
// summary and artifacts are ready to use; writing files is optional.
_ = output.Write("./licenses", summary)artifacts is a flat slice of model.LicenseArtifact including the raw Text,
so you can use permits without writing anything to disk.
Implement provider.Scanner and provider.Fetcher and register them; the
collector doesn't change:
reg := permits.DefaultRegistry(opts) // or provider.NewRegistry()
reg.Register(myCargoScanner, myCargoFetcher)
c := permits.NewCollector(reg, opts)A Scanner parses a lockfile into []model.Dependency; a Fetcher turns one
model.Dependency into []model.LicenseArtifact. provider/npm and
provider/gomod are working examples to copy.
make test # go test ./...
make lint # go vet + gofmt check