# License Check

> Runs an end-to-end license-compliance check on the current project per spec/project/license-check/ and produce a license-check audit artifact. Dispatches license-check-scanner agent for the read-only inventory (SBOM with resolved licenses, SPDX identification, category classification), then applies the permissive-leaning allow/review/deny policy gate against the project's own outbound license, drives per-finding remediation (replace / exception with rationale / satisfy the obligation), verifies attribution/NOTICE and REUSE, records AI provenance, and writes the artifact under .audits/license-check/. Invoke when the user asks to "run a license check," "check license compliance," "audit licenses," "do a Lizenzcheck," "prüfe die Lizenzen," or for a pre-PR / pre-release license gate. Don't use for CVE / vulnerability scanning (that's dependency-audit) or for choosing the project's own outbound license. Supports resume on re-invocation per spec/claude/resumable-work/.

- Skill: `nolte/license-check` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add nolte/license-check`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nolte/license-check/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: nolte (https://skillmd.com/u/nolte)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nolte/license-check

---


# License Check

Run the end-to-end license-compliance process on the current project and produce a single license-check audit artifact. This skill reports, gates, and (with confirmation) drives remediation; it never edits a `LICENSE`, allowlist, or dependency on its own.

Implements `spec/project/license-check/` — the spec defines the pipeline, the SPDX anchoring, the license categories and their obligations, the compatibility rules, the default policy, and the AI-provenance requirements. This skill is the license-compliance **authority**; `dependency-audit`'s license pass implements this spec's policy for the dependency slice only.

## German trigger phrases

This skill also triggers on equivalent German-language requests, including:

- "Lizenzcheck durchführen" / "ausführlichen Lizenzcheck machen"
- "Lizenz-Compliance prüfen"
- "Lizenzen auditieren"

## User-language policy

Detect the user's language from their message and respond in it. The audit artifact uses English section headings (so downstream tooling can parse it reliably); prose around it is localised.

## Inputs

- **Repo root**: default is the current working directory.
- **Outbound license**: read from the root `LICENSE`. This is the compatibility anchor; every conveyed dependency is checked against it. Stop and ask when no `LICENSE` exists rather than guessing.
- **Policy**: the permissive-leaning default from `spec/project/license-check/` §Default policy, overridable only by a recorded, rationale-bearing repository policy.

## The pipeline

Run the spec's ordered chain; each stage consumes the previous one:

`Inventory/Discovery → SBOM → SPDX identification → classification → policy gate → remediation → attribution/NOTICE → continuous CI check`

The SBOM stage is generated by this skill (it may install and write); the inventory, SPDX-identification, and classification stages are delegated to the read-only scanner agent; the policy gate through the CI check stay in this skill.

## Operations

### 0. Generate the SBOM, then dispatch the read-only scan agent

First generate the SBOM yourself — a skill may install and write, the read-only scanner agent may not. Run the repo's `task license:sbom` target when it exists; otherwise generate per stack (Python: `cyclonedx-py environment` against an ephemeral venv, removed after; Node: `cyclonedx-node-npm`; Go: `cyclonedx-gomod`). Use the Python `environment` mode, not `requirements` mode, because only the former resolves licenses, including transitive ones.

Then dispatch `license-check-scanner` (Agent) for the read-only inventory, passing the repo root, the generated SBOM path, and the outbound license read in Inputs. The agent reads the SBOM, maps every component to a canonical SPDX identifier, classifies each into a category, resolves any remaining gaps read-only, and returns the project's own outbound license plus REUSE state and AI-provenance hints. Wait for its inventory before applying the gate.

### 1. Determine conveyed vs. arm's-length

Per `spec/project/license-check/` §Scope, mark every component the scanner returned as either *conveyed* (shipped, linked, or offered over a network as part of the product) or *executed at arm's length* (build / dev / docs / CI tool, not shipped). When the use context can't be determined, record `unknown-use-context` and route the component to `review`. This distinction decides whether a strong-/network-copyleft finding is `deny` or `review`.

### 2. Apply the policy gate

Apply the three-tier default policy from the spec, anchored to the outbound license:

- **allow** (passes automatically): the permissive and public-domain categories (the default allowlist is SPDX-anchored and seeded from the CNCF set: `0BSD, BSD-2-Clause, BSD-3-Clause, MIT, MIT-0, ISC, Apache-2.0, PSF-2.0, Python-2.0, PostgreSQL, Zlib, X11, Unlicense, CC0-1.0`).
- **review** (manual decision, gate is `blocked` not `pass`): weak/file-level copyleft; source-available / restricted; open-weight model licenses; the BSD-4-Clause advertising-clause exception; any `LicenseRef-*` / `NOASSERTION`; any combination not positively confirmed compatible with the outbound license; strong-/network-copyleft components that are *arm's-length only*.
- **deny** (fails automatically): strong copyleft (GPL family) and network copyleft (AGPL) in any *conveyed, linked, or networked* component.

Run the compatibility checks from the spec against the outbound license (for example: Apache-2.0 may go into a GPLv3 work but not the reverse; Apache-2.0 is incompatible with GPLv2). Route any combination you can't positively confirm to `review`. Never downgrade a category on local judgement; disagreement is a recorded exception with rationale, not a reclassification.

### 3. Remediate (per non-`allow` finding, with confirmation)

Per `spec/project/license-check/` §Remediation, every non-`allow` finding gets exactly one of three responses inside a bounded window. Don't execute any of these without explicit confirmation:

- **replace**: swap the component for a compatibly-licensed alternative.
- **exception with rationale**: record a named, time-bounded (`valid-until`, ISO 8601) exception with a one-line rationale and the approver; permitted for `review` findings and, only with explicit sign-off, for a deny-tier override. Revisit each on its `valid-until` date; never renew without a fresh rationale.
- **satisfy the obligation**: keep the component and produce what its license requires (attribution/NOTICE entry; source-availability offer; relink-capable form for LGPL).

Never silence a finding by editing the allowlist without a rationale or by reclassifying the license.

### 4. Own-code, attribution/NOTICE, and AI provenance

- Confirm the root `LICENSE` declares a valid outbound SPDX identifier, and report the REUSE state the scanner returned (`LICENSES/<id>.txt` + per-file or blanket `REUSE.toml`). When REUSE is not configured, record it as a `SHOULD` gap.
- Verify the third-party attribution/NOTICE output the dependency licenses oblige exists. The verified cross-stack default generator is the ORT reporter's NOTICE templates (`PlainTextTemplate` / `NOTICE_SUMMARY`); for Go, `go-licenses save`. For Apache-2.0 dependencies, confirm `NOTICE` contents are propagated. When the plugin/product ships no third-party code, record that no third-party NOTICE is obligated.
- Record machine-readable AI provenance for every committed AI-generated artefact (generator / model / tier). Classify open-weight model licenses as `review`, not `allow`, and pin the model artefact version. Keep "license laundering" framing labelled as interpretation, not as a primary-source claim.

### 5. Persist the audit artifact

Per `spec/project/license-check/` §Audit artifact, write the result to the portfolio-wide `.audits/license-check/` path (default `.audits/license-check/license-YYYY-MM-DD.md`). The artifact MUST record: date; trigger (pre-PR / pre-release / manifest-change); scope (stacks and subroots checked / skipped, with reasons); the tool(s) and version(s) used; the pinned SPDX License List version; the per-component SPDX identifier, category, policy tier, and response decision; the conveyed/arm's-length status of every strong-/network-copyleft component; and the Git revision checked. Link to the prior artifact so the progression stays traceable.

### 6. Gate result and CI

Report the gate result: `pass` (no `deny`, every non-`allow` finding has a response) or `blocked` (open `review` findings or an unaddressed `deny`). Per the spec, the check runs in CI on changes to a dependency manifest, lockfile, the `LICENSE` file, or committed AI-generated artefacts, and before every release tag; it shares cadence and artifact conventions with `dependency-audit` rather than duplicating it.

## Report shape

```text
# License Check

Scope: <repo root>, stacks: <list>, <n> components (<d> direct, <t> transitive); skipped: <list with reasons>
Trigger: <pre-PR | pre-release | manifest-change>
Outbound license (anchor): <SPDX id>
REUSE: <compliant | non-compliant | not configured>
SPDX List version: <version>
Tools (with versions): <list>
Git revision: <sha>

## Policy gate
- allow: <count>
- review: <count>  (blocked until each has a response)
- deny: <count>    (must be replaced or under a signed-off, time-bounded exception)

## Findings (non-allow only, grouped by tier)
### deny — <count>
- **<name>@<version>** — <SPDX id> — <category> — use-context: <conveyed|arm's-length|unknown> — response: <replace | exception (valid-until <date>, approver <name>) | satisfy>
### review — <count>
(same structure)

## Own code & attribution
- Outbound LICENSE: <SPDX id> (valid: yes/no)
- REUSE: <state> (LICENSES/, REUSE.toml)
- Third-party NOTICE: <present | not obligated (no third-party code shipped) | missing>

## AI provenance
- <artefact> — <generator/model> — output license: <SPDX> — provenance recorded: yes/no
- Open-weight model licenses: <list as review, or "none">

## Verdict
<pass | blocked> — <one-line rationale>
```

## Gotchas

- **A license SBOM needs resolved licenses, not just a dependency list.** `cyclonedx-py requirements` lists components without licenses; only `cyclonedx-py environment` (against an installed venv) or the repo's `task license:sbom` target resolves licenses, including transitive ones. The scanner prefers `task license:sbom`; verify the SBOM's "components with resolved license" count matches the component count before trusting a clean result.
- **Transitive copyleft hides behind permissive top-level manifests.** A top-level-only scan misses weak-copyleft transitives (for example `certifi` / `pathspec` under MPL-2.0). Always gate on the transitive SBOM, not the manifest.
- **The conveyed/arm's-length split is the load-bearing decision for copyleft.** The same GPL tool is `deny` when shipped and `review` when only executed at build time. Decide it per component; default to `review` (`unknown-use-context`) when unsure, never to `allow`.
- **`go-licenses` and Syft can mislabel.** `go-licenses` warns on non-Go code (which can conceal further requirements) and can fail to resolve a license URL; Syft historically marked unmatched license text as "unlicensed" before its `include-unknown-license-content` option. Treat any unresolved component as `review`, never a silent pass.
- **The FSF static=dynamic-linking position is the conservative default, not settled law.** Use it to gate, but record in the artifact that it is the FSF interpretation.

## Examples

- Read `examples/01-permissive-python-pass.md` when running the first check on a permissive project that should pass cleanly.
- Read `examples/02-transitive-mpl-review.md` when a transitive dependency carries weak (file-level) copyleft and must be routed to `review`.
- Read `examples/03-conveyed-gpl-deny.md` when a conveyed component carries strong copyleft and the gate must `deny` it.

## Resumability

Per `spec/claude/resumable-work/`, this skill is `resumable: true`. State is persisted to `.resume/license-check/<run-id>.yml` after every successful user-approval gate and after each named phase boundary. On re-invocation, scan that directory for files with `status: in_progress` whose `inputs:` snapshot matches the current invocation; if one matches, prompt the operator with `Resume run <run_id> from phase <phase> (last checkpoint <last_checkpoint_at>)? [resume / start-new / discard]`. The state-file envelope (`schema_version`, `run_id`, `inputs`, `phase`, `decisions[]`, `status`, ...) and the fail-closed semantics on schema or YAML errors are load-bearing in the spec; don't duplicate those rules here.

## Source triangulation

Per `spec/claude/research-triangulate/`, before this skill presents any **repo-external assertion** beyond a tool's own machine-readable output — a license's compatibility ruling, an SPDX identifier resolved by hand, or a generator's output-license terms — triangulate it instead of trusting a single source:

- **Independent sources by blast radius.** At least two independent sources; at least three when the assertion will direct a write outside the working copy (a deny-tier override, a sister-repo path).
- **Anchor in SPDX.** Resolve a license's full text via its SPDX identifier (`spdx.org/licenses/<ID>.json`) so the obligations behind any finding are checkable; pin the SPDX List version in the artifact.
- **Surface conflicts, never silent-vote.** When sources disagree on a license's category or compatibility, name the most likely explanation and let the operator decide.
- **Mark `unverified` when under-triangulated.** If the required source count is unreachable, mark the assertion `unverified` and route the component to `review`.

## Hard rules

- **Never** edit a `LICENSE`, `REUSE.toml`, allowlist, NOTICE, or dependency without explicit user confirmation. This skill reports and gates; mutations are a confirmed follow-up.
- **Never** place a conveyed or networked strong-/network-copyleft component in `allow`. It is `deny` unless replaced or under a signed-off, time-bounded exception.
- **Never** downgrade a license's category on local judgement. Disagreement is a recorded exception with rationale, not a reclassification.
- **Never** treat an unresolved or unmatched license as a clean pass. It is `review`.
- **Never** classify an open-weight model license (OpenRAIL / Llama / Gemma custom terms) as `allow`; it is `review`, with the model artefact version pinned.
- **Never** run a CVE / vulnerability scan or duplicate `dependency-audit`; consume its license pass as an implementer of this spec's policy, not as a competing authority.
- **Always** anchor every finding in a canonical SPDX identifier (or an explicit `LicenseRef-*` / `NOASSERTION`) whose full text is resolvable.
- **Always** gate on the transitive SBOM, and record the conveyed/arm's-length status of every copyleft finding.
- **Always** persist the audit artifact under `.audits/license-check/` with the pinned SPDX List version and the Git revision.
- When `spec/project/license-check/` and this skill disagree, the spec wins; this skill needs the update.

## Why this is a skill, not an agent

This skill follows the hybrid pattern: the read-only inventory phase is delegated to the `license-check-scanner` agent (context-window isolation, tool restriction), while the policy gate and follow-up actions stay in the skill.

- **Orchestration role**: typical callers run this as one step inside a larger flow (pre-PR gate, release cut, periodic compliance review); the output flows back into the main conversation so the operator can triage.
- **Mid-flow interactivity**: the remediation actions in Step 3 need per-finding user approval — replacing a dependency, recording a dated exception, generating a NOTICE — so the interactivity favours the skill side.
- **Persistent artifact**: the deliverable is an on-disk audit artifact under `.audits/license-check/`; skills own persistent state.
- **Hybrid split**: the pure inventory half (detect stack, generate SBOM, resolve SPDX, classify) is self-contained and benefits from context-window isolation; the `license-check-scanner` agent handles it. The policy gate and remediation stay here so the operator can decide each tier interactively.

