Skip to content
Back to skills

License Clash Mediator

ASecurity

Build a dependency-license graph and flag copyleft/permissive conflicts plus missing license metadata before they become legal incidents.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 27, 2026
developmentnodeexpresssecurity

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 27, 2026

npx -y skills add RMAzrim/opencode-artes --skill license-clash-mediator --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of License Clash Mediator?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for License Clash Mediator
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rmazrim-license-clash-mediator/badge)](https://www.skillsdirectory.com/skills/rmazrim-license-clash-mediator)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: License Clash Mediator
description: Build a dependency-license graph and flag copyleft/permissive conflicts plus missing license metadata before they become legal incidents.
metadata:
  source: skills/license-clash-mediator/license-clash-mediator.md
---

# License Clash Mediator

## 1. System Architecture & Prerequisites

Shipping a dependency with an incompatible license — GPL into a proprietary
binary, a "no commercial use" license into a SaaS — is a legal landmine that a
dependency audit never catches *as a license issue*. The Mediator walks your
lockfile, resolves each package's license expression, classifies permissiveness
(there is a small hard-coded police table; extend it for your org), and emits a
classified clash report. Node 18+ stdlib.

License classifier (expression → class):

- `PERMISSIVE`: MIT, Apache-2.0, BSD-*, ISC
- `WEAK_COPYLEFT`: LGPL-*, MPL-*
- `STRONG_COPYLEFT`: GPL-*, AGPL-*
- `RESTRICTED`: CC-BY-NC*, BUSL-*, "SEE LICENSE IN ..."
- `UNKNOWN`: missing/`UNLICENSED`/custom

## 2. Input/Output Data Contracts

Input: `package-lock.json` with registry metadata (uses `license` field from
each version from a local `node_modules/<pkg>/package.json` when available —
no network required).

Output `LICENSE_CLASH_REPORT.md` + `license-report.json`:

```json
{ "deps": 214, "unknown": 22, "flagged": [ { "name": "lib-mp4", "license": "SEE LICENSE IN COPYING", "class": "RESTRICTED", "consumedBy": ["video-worker", "ingest"] } ], "conflicts": [ { "a": "agg-lib", "la": "GPL-3.0", "b": "proprietary", "note": "strong copyleft in closed-source distribution" } ] }
```

Exit: `0` = no unknown/restricted/copyleft-in-proprietary flags,
`1` = flags exist, `2` = input unreadable.

## 3. Production Reference Implementation

```js
// license-clash-mediator.js
const fs = require('fs');
const path = require('path');

const root = process.argv[2] || '.';
const PROPRIETARY = process.argv.includes('--proprietary'); // binary/SaaS closed-source

function classify(expr) {
  const e = String(expr || '').toUpperCase();
  if (/(MIT|APACHE|BSD|ISC|MPL|UNLICEN(?:SE)?A|ZERO|UNLICENSE)/.test(e) && !/CC-|BUSL|SEE LICEN|COMMERCIAL/.test(e)) return 'PERMISSIVE';
  if (/(LGPL|MPL)/.test(e)) return 'WEAK_COPYLEFT';
  if (/(AGPL|GPL)/.test(e)) return 'STRONG_COPYLEFT';
  if (/(CC-|BUSL|SEE DESIGN|SEE LICENSE|COMMERCIAL|UNKNOWN)/.test(e)) return 'UNKNOWN_OR_RESTRICTED';
  if (e === '' || e === 'UNLICENSED' || e === 'PROPRIETARY') return 'UNKNOWN_OR_RESTRICTED';
  return 'UNKNOWN_OR_RESTRICTED';
}

function collect() {
  const lockPath = path.join(root, 'package-lock.json');
  if (!fs.existsSync(lockPath)) { console.error(`package-lock.json not found in ${root}`); process.exit(2); }
  const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
  const entries = [];
  for (const [p, meta] of Object.entries(lock.packages || {})) {
    if (!p.startsWith('node_modules/')) continue;
    const name = p.slice('node_modules/'.length).split('/node_modules/').pop();
    let license = null;
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(root, p, 'package.json'), 'utf-8'));
      license = pkg.license;
    } catch {}
    entries.push({ name, version: meta.version || '?', license });
  }
  return entries;
}

function main() {
  const deps = collect();
  const flagged = [];
  const unknown = [];
  for (const d of deps) {
    const cls = classify(d.license);
    d.class = cls;
    if (cls === 'UNKNOWN_OR_RESTRICTED') {
      flagged.push(d);
      if (!d.license || d.license === 'UNLICENSED') unknown.push(d.name);
    }
  }
  // Track reverse dependencies by scanning each package.json for direct deps.
  const consumers = new Map();
  for (const k of Object.keys(JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf-8')).packages || {})) {
    if (!k.startsWith('node_modules/')) continue;
    const meta = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf-8')).packages[k];
    if (meta && meta.dependencies) {
      for (const depName of Object.keys(meta.dependencies)) {
        if (!consumers.has(depName)) consumers.set(depName, []);
        consumers.get(depName).push(k.split('/node_modules/').pop());
      }
    }
  }
  for (const f of flagged) f.consumedBy = consumers.get(f.name) || ['<root>'];

  const conflicts = PROPRIETARY
    ? deps.filter((d) => d.class === 'STRONG_COPYLEFT').map((d) => ({ a: d.name, la: d.license, b: 'proprietary', note: 'AGPL/GPL in closed-source distribution' }))
    : [];

  const report = { deps: deps.length, unknown: unknown.length, flagged, conflicts };
  fs.writeFileSync('license-report.json', JSON.stringify(report, null, 2));
  const md = ['# License Clash Report', '',
    `Dependencies: **${deps.length}** · Unknown/flagged: **${flagged.length}** · Conflicts: **${conflicts.length}**`, '',
    '## Flagged (unknown/restricted licences)', '',
    ...flagged.slice(0, 30).map((f) => `- \`${f.name}@${f.version}\` — \`${f.license || 'no license'}\` (used by ${f.consumedBy.join(', ')})`),
    '',
    conflicts.length ? '## Copyleft in proprietary distribution' + '\n' + conflicts.map((c) => `- \`${c.a}\` (${c.la}) → ${c.b}: ${c.note}`).join('\n') : 'No strong-copyleft conflict with proprietary distribution.',
  ].join('\n');
  fs.writeFileSync('LICENSE_CLASH_REPORT.md', md);
  console.log(md);
  process.exit((flagged.length || conflicts.length) ? 1 : 0);
}

main();
```

## 4. Execution Protocol & Step-by-Step Workflow

1. Run `node license-clash-mediator.js .` — add `--proprietary` when the target
   distribution is closed-source (binary app, internal SaaS).
2. Triage `flagged` in this order:
   - `unknown`/no license → resolve via the upstream repo, record the decision;
   - `RESTRICTED` (CC-NC, BUSL, "SEE LICENSE") → substitution candidates or
     legal sign-off before ship;
   - `STRONG_COPYLEFT` + proprietary → this is the days-of-legal-drama case;
     pick a compatible replacement.
3. Where flagged is acceptable (e.g. dev-only tooling), maintain an
   `allowlist.json` of package names the gate ignores.
4. Wire as a CI gate on every lockfile PR so a single `npm i bad-package`
   cannot slip through.
5. Re-run monthly; the `license-report.json` is the artifact your security
   review signs.

## 5. Edge Cases & Error Handling

- SPDX expressions may carry `OR`/`AND` (e.g. `(MIT OR Apache-2.0)`); the regex
   classifier treats the string conservatively — flag for a human when in doubt.
- Scoped proxies (`@scope/pkg`) resolve from the full node path, so nested
   names never alias.
- Missing `package.json` under `node_modules` (pruned installs) records the
   license as unknown rather than crashing; re-run after `npm ci` for accuracy.
- `UNLICENSED` (explicit private) and `SEE LICENSE IN ...` are both routed to
   human review — the report distinguishes them by name.
- The reverse-consumer scan reads the lockfile's `dependencies` maps; conform
   lockfiles v1 without a `packages` block trigger exit `2` with a hint to
   regenerate the lockfile.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.