|
| 1 | +--- |
| 2 | +name: 'Dependency License Checker' |
| 3 | +description: 'Scans newly added dependencies for license compliance (GPL, AGPL, etc.) at session end' |
| 4 | +tags: ['compliance', 'license', 'dependencies', 'session-end'] |
| 5 | +--- |
| 6 | + |
| 7 | +# Dependency License Checker Hook |
| 8 | + |
| 9 | +Scans newly added dependencies for license compliance at the end of a GitHub Copilot coding agent session, flagging copyleft and restrictive licenses (GPL, AGPL, SSPL, etc.) before they get committed. |
| 10 | + |
| 11 | +## Overview |
| 12 | + |
| 13 | +AI coding agents may add new dependencies during a session without considering license implications. This hook acts as a compliance safety net by detecting new dependencies across multiple ecosystems, looking up their licenses, and checking them against a configurable blocked list of copyleft and restrictive licenses. |
| 14 | + |
| 15 | +## Features |
| 16 | + |
| 17 | +- **Multi-ecosystem support**: npm, pip, Go, Ruby, and Rust dependency detection |
| 18 | +- **Two modes**: `warn` (log only) or `block` (exit non-zero to prevent commit) |
| 19 | +- **Configurable blocked list**: Default copyleft set with full SPDX variant coverage |
| 20 | +- **Allowlist support**: Skip known-acceptable packages via `LICENSE_ALLOWLIST` |
| 21 | +- **Smart detection**: Uses `git diff` to detect only newly added dependencies |
| 22 | +- **Multiple lookup strategies**: Local cache, package manager CLI, with fallback to UNKNOWN |
| 23 | +- **Structured logging**: JSON Lines output for integration with monitoring tools |
| 24 | +- **Timeout protection**: Each license lookup wrapped with 5-second timeout |
| 25 | +- **Zero mandatory dependencies**: Uses standard Unix tools; optional `jq` for better JSON parsing |
| 26 | + |
| 27 | +## Installation |
| 28 | + |
| 29 | +1. Copy the hook folder to your repository: |
| 30 | + |
| 31 | + ```bash |
| 32 | + cp -r hooks/dependency-license-checker .github/hooks/ |
| 33 | + ``` |
| 34 | + |
| 35 | +2. Ensure the script is executable: |
| 36 | + |
| 37 | + ```bash |
| 38 | + chmod +x .github/hooks/dependency-license-checker/check-licenses.sh |
| 39 | + ``` |
| 40 | + |
| 41 | +3. Create the logs directory and add it to `.gitignore`: |
| 42 | + |
| 43 | + ```bash |
| 44 | + mkdir -p logs/copilot/license-checker |
| 45 | + echo "logs/" >> .gitignore |
| 46 | + ``` |
| 47 | + |
| 48 | +4. Commit the hook configuration to your repository's default branch. |
| 49 | + |
| 50 | +## Configuration |
| 51 | + |
| 52 | +The hook is configured in `hooks.json` to run on the `sessionEnd` event: |
| 53 | + |
| 54 | +```json |
| 55 | +{ |
| 56 | + "version": 1, |
| 57 | + "hooks": { |
| 58 | + "sessionEnd": [ |
| 59 | + { |
| 60 | + "type": "command", |
| 61 | + "bash": ".github/hooks/dependency-license-checker/check-licenses.sh", |
| 62 | + "cwd": ".", |
| 63 | + "env": { |
| 64 | + "LICENSE_MODE": "warn" |
| 65 | + }, |
| 66 | + "timeoutSec": 60 |
| 67 | + } |
| 68 | + ] |
| 69 | + } |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +### Environment Variables |
| 74 | + |
| 75 | +| Variable | Values | Default | Description | |
| 76 | +|----------|--------|---------|-------------| |
| 77 | +| `LICENSE_MODE` | `warn`, `block` | `warn` | `warn` logs violations only; `block` exits non-zero to prevent auto-commit | |
| 78 | +| `SKIP_LICENSE_CHECK` | `true` | unset | Disable the checker entirely | |
| 79 | +| `LICENSE_LOG_DIR` | path | `logs/copilot/license-checker` | Directory where check logs are written | |
| 80 | +| `BLOCKED_LICENSES` | comma-separated SPDX IDs | copyleft set | Licenses to flag as violations | |
| 81 | +| `LICENSE_ALLOWLIST` | comma-separated | unset | Package names to skip (e.g., `linux-headers,glibc`) | |
| 82 | + |
| 83 | +## How It Works |
| 84 | + |
| 85 | +1. When a Copilot coding agent session ends, the hook executes |
| 86 | +2. Runs `git diff HEAD` against manifest files (package.json, requirements.txt, go.mod, etc.) |
| 87 | +3. Extracts newly added package names from the diff output |
| 88 | +4. Looks up each package's license using local caches and package manager CLIs |
| 89 | +5. Checks each license against the blocked list using case-insensitive substring matching |
| 90 | +6. Skips packages in the allowlist before flagging |
| 91 | +7. Reports findings in a formatted table with package, ecosystem, license, and status |
| 92 | +8. Writes a structured JSON log entry for audit purposes |
| 93 | +9. In `block` mode, exits non-zero to signal the agent to stop before committing |
| 94 | + |
| 95 | +## Supported Ecosystems |
| 96 | + |
| 97 | +| Ecosystem | Manifest File | Primary Lookup | Fallback | |
| 98 | +|-----------|--------------|----------------|----------| |
| 99 | +| npm/yarn/pnpm | `package.json` | `node_modules/<pkg>/package.json` license field | `npm view <pkg> license` | |
| 100 | +| pip | `requirements.txt`, `pyproject.toml` | `pip show <pkg>` License field | UNKNOWN | |
| 101 | +| Go | `go.mod` | LICENSE file in module cache (keyword match) | UNKNOWN | |
| 102 | +| Ruby | `Gemfile` | `gem spec <pkg> license` | UNKNOWN | |
| 103 | +| Rust | `Cargo.toml` | `cargo metadata` license field | UNKNOWN | |
| 104 | + |
| 105 | +## Default Blocked Licenses |
| 106 | + |
| 107 | +The following licenses are blocked by default (copyleft and restrictive): |
| 108 | + |
| 109 | +- **GPL**: GPL-2.0, GPL-2.0-only, GPL-2.0-or-later, GPL-3.0, GPL-3.0-only, GPL-3.0-or-later |
| 110 | +- **AGPL**: AGPL-1.0, AGPL-3.0, AGPL-3.0-only, AGPL-3.0-or-later |
| 111 | +- **LGPL**: LGPL-2.0, LGPL-2.1, LGPL-2.1-only, LGPL-2.1-or-later, LGPL-3.0, LGPL-3.0-only, LGPL-3.0-or-later |
| 112 | +- **Other**: SSPL-1.0, EUPL-1.1, EUPL-1.2, OSL-3.0, CPAL-1.0, CPL-1.0 |
| 113 | +- **Creative Commons (restrictive)**: CC-BY-SA-4.0, CC-BY-NC-4.0, CC-BY-NC-SA-4.0 |
| 114 | + |
| 115 | +Override with `BLOCKED_LICENSES` to customize. |
| 116 | + |
| 117 | +## Example Output |
| 118 | + |
| 119 | +### Clean scan (no new dependencies) |
| 120 | + |
| 121 | +``` |
| 122 | +✅ No new dependencies detected |
| 123 | +``` |
| 124 | + |
| 125 | +### Clean scan (all compliant) |
| 126 | + |
| 127 | +``` |
| 128 | +🔍 Checking licenses for 3 new dependency(ies)... |
| 129 | +
|
| 130 | + PACKAGE ECOSYSTEM LICENSE STATUS |
| 131 | + ------- --------- ------- ------ |
| 132 | + express npm MIT OK |
| 133 | + lodash npm MIT OK |
| 134 | + axios npm MIT OK |
| 135 | +
|
| 136 | +✅ All 3 dependencies have compliant licenses |
| 137 | +``` |
| 138 | + |
| 139 | +### Violations detected (warn mode) |
| 140 | + |
| 141 | +``` |
| 142 | +🔍 Checking licenses for 2 new dependency(ies)... |
| 143 | +
|
| 144 | + PACKAGE ECOSYSTEM LICENSE STATUS |
| 145 | + ------- --------- ------- ------ |
| 146 | + react npm MIT OK |
| 147 | + readline-sync npm GPL-3.0 BLOCKED |
| 148 | +
|
| 149 | +⚠️ Found 1 license violation(s): |
| 150 | +
|
| 151 | + - readline-sync (npm): GPL-3.0 |
| 152 | +
|
| 153 | +💡 Review the violations above. Set LICENSE_MODE=block to prevent commits with license issues. |
| 154 | +``` |
| 155 | + |
| 156 | +### Violations detected (block mode) |
| 157 | + |
| 158 | +``` |
| 159 | +🔍 Checking licenses for 2 new dependency(ies)... |
| 160 | +
|
| 161 | + PACKAGE ECOSYSTEM LICENSE STATUS |
| 162 | + ------- --------- ------- ------ |
| 163 | + flask pip BSD-3-Clause OK |
| 164 | + copyleft-lib pip AGPL-3.0 BLOCKED |
| 165 | +
|
| 166 | +⚠️ Found 1 license violation(s): |
| 167 | +
|
| 168 | + - copyleft-lib (pip): AGPL-3.0 |
| 169 | +
|
| 170 | +🚫 Session blocked: resolve license violations above before committing. |
| 171 | + Set LICENSE_MODE=warn to log without blocking, or add packages to LICENSE_ALLOWLIST. |
| 172 | +``` |
| 173 | + |
| 174 | +## Log Format |
| 175 | + |
| 176 | +Check events are written to `logs/copilot/license-checker/check.log` in JSON Lines format: |
| 177 | + |
| 178 | +```json |
| 179 | +{"timestamp":"2026-03-17T10:30:00Z","event":"license_check_complete","mode":"warn","dependencies_checked":3,"violation_count":1,"violations":[{"package":"readline-sync","ecosystem":"npm","license":"GPL-3.0","status":"BLOCKED"}]} |
| 180 | +``` |
| 181 | + |
| 182 | +```json |
| 183 | +{"timestamp":"2026-03-17T10:30:00Z","event":"license_check_complete","mode":"warn","status":"clean","dependencies_checked":0} |
| 184 | +``` |
| 185 | + |
| 186 | +## Pairing with Other Hooks |
| 187 | + |
| 188 | +This hook pairs well with: |
| 189 | + |
| 190 | +- **Secrets Scanner**: Run secrets scanning first, then license checking, before auto-commit |
| 191 | +- **Session Auto-Commit**: When both are installed, order them so that `dependency-license-checker` runs first. Set `LICENSE_MODE=block` to prevent auto-commit when violations are detected. |
| 192 | + |
| 193 | +## Customization |
| 194 | + |
| 195 | +- **Modify blocked licenses**: Set `BLOCKED_LICENSES` to a custom comma-separated list of SPDX IDs |
| 196 | +- **Allowlist packages**: Use `LICENSE_ALLOWLIST` for known-acceptable packages with copyleft licenses |
| 197 | +- **Change log location**: Set `LICENSE_LOG_DIR` to route logs to your preferred directory |
| 198 | +- **Add ecosystems**: Extend the detection and lookup sections in `check-licenses.sh` |
| 199 | + |
| 200 | +## Disabling |
| 201 | + |
| 202 | +To temporarily disable the checker: |
| 203 | + |
| 204 | +- Set `SKIP_LICENSE_CHECK=true` in the hook environment |
| 205 | +- Or remove the `sessionEnd` entry from `hooks.json` |
| 206 | + |
| 207 | +## Limitations |
| 208 | + |
| 209 | +- License detection relies on manifest file diffs; dependencies added outside standard manifest files are not detected |
| 210 | +- License lookup requires the package manager CLI or local cache to be available |
| 211 | +- Compound SPDX expressions (e.g., `MIT OR GPL-3.0`) are flagged if any component matches the blocked list |
| 212 | +- Does not perform deep transitive dependency license analysis |
| 213 | +- Network lookups (npm view, etc.) may fail in offline or restricted environments |
| 214 | +- Requires `git` to be available in the execution environment |
0 commit comments