From 9245a107ff9e49ad1561fe9f24a74cd522204fb2 Mon Sep 17 00:00:00 2001
From: Ajith Raghavan <37246967+ajithraghavan@users.noreply.github.com>
Date: Tue, 17 Mar 2026 14:31:46 +0530
Subject: [PATCH] Dependency License Checker Hook
---
docs/README.hooks.md | 1 +
hooks/dependency-license-checker/README.md | 214 +++++++++++
.../check-licenses.sh | 354 ++++++++++++++++++
hooks/dependency-license-checker/hooks.json | 16 +
4 files changed, 585 insertions(+)
create mode 100644 hooks/dependency-license-checker/README.md
create mode 100755 hooks/dependency-license-checker/check-licenses.sh
create mode 100644 hooks/dependency-license-checker/hooks.json
diff --git a/docs/README.hooks.md b/docs/README.hooks.md
index e23200323..1d11a84a9 100644
--- a/docs/README.hooks.md
+++ b/docs/README.hooks.md
@@ -31,6 +31,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-hooks) for guidelines on how to
| Name | Description | Events | Bundled Assets |
| ---- | ----------- | ------ | -------------- |
+| [Dependency License Checker](../hooks/dependency-license-checker/README.md) | Scans newly added dependencies for license compliance (GPL, AGPL, etc.) at session end | sessionEnd | `check-licenses.sh`
`hooks.json` |
| [Governance Audit](../hooks/governance-audit/README.md) | Scans Copilot agent prompts for threat signals and logs governance events | sessionStart, sessionEnd, userPromptSubmitted | `audit-prompt.sh`
`audit-session-end.sh`
`audit-session-start.sh`
`hooks.json` |
| [Secrets Scanner](../hooks/secrets-scanner/README.md) | Scans files modified during a Copilot coding agent session for leaked secrets, credentials, and sensitive data | sessionEnd | `hooks.json`
`scan-secrets.sh` |
| [Session Auto-Commit](../hooks/session-auto-commit/README.md) | Automatically commits and pushes changes when a Copilot coding agent session ends | sessionEnd | `auto-commit.sh`
`hooks.json` |
diff --git a/hooks/dependency-license-checker/README.md b/hooks/dependency-license-checker/README.md
new file mode 100644
index 000000000..6d54f0ccd
--- /dev/null
+++ b/hooks/dependency-license-checker/README.md
@@ -0,0 +1,214 @@
+---
+name: 'Dependency License Checker'
+description: 'Scans newly added dependencies for license compliance (GPL, AGPL, etc.) at session end'
+tags: ['compliance', 'license', 'dependencies', 'session-end']
+---
+
+# Dependency License Checker Hook
+
+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.
+
+## Overview
+
+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.
+
+## Features
+
+- **Multi-ecosystem support**: npm, pip, Go, Ruby, and Rust dependency detection
+- **Two modes**: `warn` (log only) or `block` (exit non-zero to prevent commit)
+- **Configurable blocked list**: Default copyleft set with full SPDX variant coverage
+- **Allowlist support**: Skip known-acceptable packages via `LICENSE_ALLOWLIST`
+- **Smart detection**: Uses `git diff` to detect only newly added dependencies
+- **Multiple lookup strategies**: Local cache, package manager CLI, with fallback to UNKNOWN
+- **Structured logging**: JSON Lines output for integration with monitoring tools
+- **Timeout protection**: Each license lookup wrapped with 5-second timeout
+- **Zero mandatory dependencies**: Uses standard Unix tools; optional `jq` for better JSON parsing
+
+## Installation
+
+1. Copy the hook folder to your repository:
+
+ ```bash
+ cp -r hooks/dependency-license-checker .github/hooks/
+ ```
+
+2. Ensure the script is executable:
+
+ ```bash
+ chmod +x .github/hooks/dependency-license-checker/check-licenses.sh
+ ```
+
+3. Create the logs directory and add it to `.gitignore`:
+
+ ```bash
+ mkdir -p logs/copilot/license-checker
+ echo "logs/" >> .gitignore
+ ```
+
+4. Commit the hook configuration to your repository's default branch.
+
+## Configuration
+
+The hook is configured in `hooks.json` to run on the `sessionEnd` event:
+
+```json
+{
+ "version": 1,
+ "hooks": {
+ "sessionEnd": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/dependency-license-checker/check-licenses.sh",
+ "cwd": ".",
+ "env": {
+ "LICENSE_MODE": "warn"
+ },
+ "timeoutSec": 60
+ }
+ ]
+ }
+}
+```
+
+### Environment Variables
+
+| Variable | Values | Default | Description |
+|----------|--------|---------|-------------|
+| `LICENSE_MODE` | `warn`, `block` | `warn` | `warn` logs violations only; `block` exits non-zero to prevent auto-commit |
+| `SKIP_LICENSE_CHECK` | `true` | unset | Disable the checker entirely |
+| `LICENSE_LOG_DIR` | path | `logs/copilot/license-checker` | Directory where check logs are written |
+| `BLOCKED_LICENSES` | comma-separated SPDX IDs | copyleft set | Licenses to flag as violations |
+| `LICENSE_ALLOWLIST` | comma-separated | unset | Package names to skip (e.g., `linux-headers,glibc`) |
+
+## How It Works
+
+1. When a Copilot coding agent session ends, the hook executes
+2. Runs `git diff HEAD` against manifest files (package.json, requirements.txt, go.mod, etc.)
+3. Extracts newly added package names from the diff output
+4. Looks up each package's license using local caches and package manager CLIs
+5. Checks each license against the blocked list using case-insensitive substring matching
+6. Skips packages in the allowlist before flagging
+7. Reports findings in a formatted table with package, ecosystem, license, and status
+8. Writes a structured JSON log entry for audit purposes
+9. In `block` mode, exits non-zero to signal the agent to stop before committing
+
+## Supported Ecosystems
+
+| Ecosystem | Manifest File | Primary Lookup | Fallback |
+|-----------|--------------|----------------|----------|
+| npm/yarn/pnpm | `package.json` | `node_modules//package.json` license field | `npm view license` |
+| pip | `requirements.txt`, `pyproject.toml` | `pip show ` License field | UNKNOWN |
+| Go | `go.mod` | LICENSE file in module cache (keyword match) | UNKNOWN |
+| Ruby | `Gemfile` | `gem spec license` | UNKNOWN |
+| Rust | `Cargo.toml` | `cargo metadata` license field | UNKNOWN |
+
+## Default Blocked Licenses
+
+The following licenses are blocked by default (copyleft and restrictive):
+
+- **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
+- **AGPL**: AGPL-1.0, AGPL-3.0, AGPL-3.0-only, AGPL-3.0-or-later
+- **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
+- **Other**: SSPL-1.0, EUPL-1.1, EUPL-1.2, OSL-3.0, CPAL-1.0, CPL-1.0
+- **Creative Commons (restrictive)**: CC-BY-SA-4.0, CC-BY-NC-4.0, CC-BY-NC-SA-4.0
+
+Override with `BLOCKED_LICENSES` to customize.
+
+## Example Output
+
+### Clean scan (no new dependencies)
+
+```
+✅ No new dependencies detected
+```
+
+### Clean scan (all compliant)
+
+```
+🔍 Checking licenses for 3 new dependency(ies)...
+
+ PACKAGE ECOSYSTEM LICENSE STATUS
+ ------- --------- ------- ------
+ express npm MIT OK
+ lodash npm MIT OK
+ axios npm MIT OK
+
+✅ All 3 dependencies have compliant licenses
+```
+
+### Violations detected (warn mode)
+
+```
+🔍 Checking licenses for 2 new dependency(ies)...
+
+ PACKAGE ECOSYSTEM LICENSE STATUS
+ ------- --------- ------- ------
+ react npm MIT OK
+ readline-sync npm GPL-3.0 BLOCKED
+
+⚠️ Found 1 license violation(s):
+
+ - readline-sync (npm): GPL-3.0
+
+💡 Review the violations above. Set LICENSE_MODE=block to prevent commits with license issues.
+```
+
+### Violations detected (block mode)
+
+```
+🔍 Checking licenses for 2 new dependency(ies)...
+
+ PACKAGE ECOSYSTEM LICENSE STATUS
+ ------- --------- ------- ------
+ flask pip BSD-3-Clause OK
+ copyleft-lib pip AGPL-3.0 BLOCKED
+
+⚠️ Found 1 license violation(s):
+
+ - copyleft-lib (pip): AGPL-3.0
+
+🚫 Session blocked: resolve license violations above before committing.
+ Set LICENSE_MODE=warn to log without blocking, or add packages to LICENSE_ALLOWLIST.
+```
+
+## Log Format
+
+Check events are written to `logs/copilot/license-checker/check.log` in JSON Lines format:
+
+```json
+{"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"}]}
+```
+
+```json
+{"timestamp":"2026-03-17T10:30:00Z","event":"license_check_complete","mode":"warn","status":"clean","dependencies_checked":0}
+```
+
+## Pairing with Other Hooks
+
+This hook pairs well with:
+
+- **Secrets Scanner**: Run secrets scanning first, then license checking, before auto-commit
+- **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.
+
+## Customization
+
+- **Modify blocked licenses**: Set `BLOCKED_LICENSES` to a custom comma-separated list of SPDX IDs
+- **Allowlist packages**: Use `LICENSE_ALLOWLIST` for known-acceptable packages with copyleft licenses
+- **Change log location**: Set `LICENSE_LOG_DIR` to route logs to your preferred directory
+- **Add ecosystems**: Extend the detection and lookup sections in `check-licenses.sh`
+
+## Disabling
+
+To temporarily disable the checker:
+
+- Set `SKIP_LICENSE_CHECK=true` in the hook environment
+- Or remove the `sessionEnd` entry from `hooks.json`
+
+## Limitations
+
+- License detection relies on manifest file diffs; dependencies added outside standard manifest files are not detected
+- License lookup requires the package manager CLI or local cache to be available
+- Compound SPDX expressions (e.g., `MIT OR GPL-3.0`) are flagged if any component matches the blocked list
+- Does not perform deep transitive dependency license analysis
+- Network lookups (npm view, etc.) may fail in offline or restricted environments
+- Requires `git` to be available in the execution environment
diff --git a/hooks/dependency-license-checker/check-licenses.sh b/hooks/dependency-license-checker/check-licenses.sh
new file mode 100755
index 000000000..6e465d43a
--- /dev/null
+++ b/hooks/dependency-license-checker/check-licenses.sh
@@ -0,0 +1,354 @@
+#!/bin/bash
+
+# Dependency License Checker Hook
+# Scans newly added dependencies for license compliance (GPL, AGPL, etc.)
+# at session end, before they get committed.
+#
+# Environment variables:
+# LICENSE_MODE - "warn" (log only) or "block" (exit non-zero on violations) (default: warn)
+# SKIP_LICENSE_CHECK - "true" to disable entirely (default: unset)
+# LICENSE_LOG_DIR - Directory for check logs (default: logs/copilot/license-checker)
+# BLOCKED_LICENSES - Comma-separated SPDX IDs to flag (default: copyleft set)
+# LICENSE_ALLOWLIST - Comma-separated package names to skip (default: unset)
+
+set -euo pipefail
+
+# ---------------------------------------------------------------------------
+# Early exit if disabled
+# ---------------------------------------------------------------------------
+if [[ "${SKIP_LICENSE_CHECK:-}" == "true" ]]; then
+ echo "⏭️ License check skipped (SKIP_LICENSE_CHECK=true)"
+ exit 0
+fi
+
+# Ensure we are in a git repository
+if ! git rev-parse --is-inside-work-tree &>/dev/null; then
+ echo "⚠️ Not in a git repository, skipping license check"
+ exit 0
+fi
+
+# ---------------------------------------------------------------------------
+# Configuration
+# ---------------------------------------------------------------------------
+MODE="${LICENSE_MODE:-warn}"
+LOG_DIR="${LICENSE_LOG_DIR:-logs/copilot/license-checker}"
+TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+FINDING_COUNT=0
+
+mkdir -p "$LOG_DIR"
+LOG_FILE="$LOG_DIR/check.log"
+
+# Default blocked licenses (copyleft / restrictive)
+DEFAULT_BLOCKED="GPL-2.0,GPL-2.0-only,GPL-2.0-or-later,GPL-3.0,GPL-3.0-only,GPL-3.0-or-later,AGPL-1.0,AGPL-3.0,AGPL-3.0-only,AGPL-3.0-or-later,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,SSPL-1.0,EUPL-1.1,EUPL-1.2,OSL-3.0,CPAL-1.0,CPL-1.0,CC-BY-SA-4.0,CC-BY-NC-4.0,CC-BY-NC-SA-4.0"
+
+BLOCKED_LIST=()
+IFS=',' read -ra BLOCKED_LIST <<< "${BLOCKED_LICENSES:-$DEFAULT_BLOCKED}"
+
+# Parse allowlist
+ALLOWLIST=()
+if [[ -n "${LICENSE_ALLOWLIST:-}" ]]; then
+ IFS=',' read -ra ALLOWLIST <<< "$LICENSE_ALLOWLIST"
+fi
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+json_escape() {
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g'
+}
+
+is_allowlisted() {
+ local pkg="$1"
+ for entry in "${ALLOWLIST[@]}"; do
+ entry=$(printf '%s' "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+ [[ -z "$entry" ]] && continue
+ if [[ "$pkg" == "$entry" ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+is_blocked_license() {
+ local license="$1"
+ local license_lower
+ license_lower=$(printf '%s' "$license" | tr '[:upper:]' '[:lower:]')
+ for blocked in "${BLOCKED_LIST[@]}"; do
+ blocked=$(printf '%s' "$blocked" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+ [[ -z "$blocked" ]] && continue
+ local blocked_lower
+ blocked_lower=$(printf '%s' "$blocked" | tr '[:upper:]' '[:lower:]')
+ # Substring match to handle SPDX variants and compound expressions
+ if [[ "$license_lower" == *"$blocked_lower"* ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+# ---------------------------------------------------------------------------
+# Phase 1: Detect new dependencies per ecosystem
+# ---------------------------------------------------------------------------
+NEW_DEPS=()
+
+# npm / yarn / pnpm — package.json
+if git diff HEAD -- package.json &>/dev/null; then
+ while IFS= read -r line; do
+ # Match added lines like: "package-name": "^1.0.0"
+ pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*"\([^"]*\)"[[:space:]]*:[[:space:]]*"[^"]*".*/\1/p')
+ if [[ -n "$pkg" && "$pkg" != "name" && "$pkg" != "version" && "$pkg" != "description" && "$pkg" != "main" && "$pkg" != "scripts" && "$pkg" != "dependencies" && "$pkg" != "devDependencies" && "$pkg" != "peerDependencies" && "$pkg" != "optionalDependencies" ]]; then
+ NEW_DEPS+=("npm:$pkg")
+ fi
+ done < <(git diff HEAD -- package.json 2>/dev/null | grep '^+' | grep -v '^+++')
+fi
+
+# pip — requirements.txt
+if git diff HEAD -- requirements.txt &>/dev/null; then
+ while IFS= read -r line; do
+ # Skip comments and blank lines
+ clean=$(printf '%s' "$line" | sed 's/^+//')
+ [[ "$clean" =~ ^[[:space:]]*# ]] && continue
+ [[ -z "$clean" ]] && continue
+ # Extract package name before ==, >=, <=, ~=, !=, etc.
+ pkg=$(printf '%s' "$clean" | sed 's/[[:space:]]*[><=!~].*//' | sed 's/[[:space:]]*//')
+ if [[ -n "$pkg" ]]; then
+ NEW_DEPS+=("pip:$pkg")
+ fi
+ done < <(git diff HEAD -- requirements.txt 2>/dev/null | grep '^+' | grep -v '^+++')
+fi
+
+# pip — pyproject.toml
+if git diff HEAD -- pyproject.toml &>/dev/null; then
+ while IFS= read -r line; do
+ # Match added lines with quoted dependency strings
+ pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*"\([A-Za-z0-9_-]*\).*/\1/p')
+ if [[ -n "$pkg" ]]; then
+ NEW_DEPS+=("pip:$pkg")
+ fi
+ done < <(git diff HEAD -- pyproject.toml 2>/dev/null | grep '^+' | grep -v '^+++')
+fi
+
+# Go — go.mod
+if git diff HEAD -- go.mod &>/dev/null; then
+ while IFS= read -r line; do
+ # Match added require entries like: + github.com/foo/bar v1.2.3
+ pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*\([a-zA-Z0-9._/-]*\.[a-zA-Z0-9._/-]*\)[[:space:]].*/\1/p')
+ if [[ -n "$pkg" && "$pkg" != "module" && "$pkg" != "go" && "$pkg" != "require" ]]; then
+ NEW_DEPS+=("go:$pkg")
+ fi
+ done < <(git diff HEAD -- go.mod 2>/dev/null | grep '^+' | grep -v '^+++')
+fi
+
+# Ruby — Gemfile
+if git diff HEAD -- Gemfile &>/dev/null; then
+ while IFS= read -r line; do
+ # Match added gem lines like: +gem 'package-name'
+ pkg=$(printf '%s' "$line" | sed -n "s/^+[[:space:]]*gem[[:space:]]*['\"\`]\([^'\"\`]*\)['\"\`].*/\1/p")
+ if [[ -n "$pkg" ]]; then
+ NEW_DEPS+=("ruby:$pkg")
+ fi
+ done < <(git diff HEAD -- Gemfile 2>/dev/null | grep '^+' | grep -v '^+++')
+fi
+
+# Rust — Cargo.toml
+if git diff HEAD -- Cargo.toml &>/dev/null; then
+ while IFS= read -r line; do
+ # Match added dependency entries like: +package-name = "1.0" or +package-name = { version = "1.0" }
+ pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*\([a-zA-Z0-9_-]*\)[[:space:]]*=.*/\1/p')
+ if [[ -n "$pkg" && "$pkg" != "name" && "$pkg" != "version" && "$pkg" != "edition" && "$pkg" != "authors" && "$pkg" != "description" && "$pkg" != "license" && "$pkg" != "repository" && "$pkg" != "rust-version" ]]; then
+ NEW_DEPS+=("rust:$pkg")
+ fi
+ done < <(git diff HEAD -- Cargo.toml 2>/dev/null | grep '^+' | grep -v '^+++')
+fi
+
+# Exit clean if no new dependencies found
+if [[ ${#NEW_DEPS[@]} -eq 0 ]]; then
+ echo "✅ No new dependencies detected"
+ printf '{"timestamp":"%s","event":"license_check_complete","mode":"%s","status":"clean","dependencies_checked":0}\n' \
+ "$TIMESTAMP" "$MODE" >> "$LOG_FILE"
+ exit 0
+fi
+
+echo "🔍 Checking licenses for ${#NEW_DEPS[@]} new dependency(ies)..."
+
+# ---------------------------------------------------------------------------
+# Phase 2: Check license per dependency
+# ---------------------------------------------------------------------------
+RESULTS=()
+
+get_license() {
+ local ecosystem="$1"
+ local pkg="$2"
+ local license="UNKNOWN"
+
+ case "$ecosystem" in
+ npm)
+ # Primary: check node_modules
+ if [[ -f "node_modules/$pkg/package.json" ]]; then
+ if command -v jq &>/dev/null; then
+ license=$(jq -r '.license // "UNKNOWN"' "node_modules/$pkg/package.json" 2>/dev/null || echo "UNKNOWN")
+ else
+ license=$(grep -oE '"license"\s*:\s*"[^"]*"' "node_modules/$pkg/package.json" 2>/dev/null | head -1 | sed 's/.*"license"\s*:\s*"//;s/"//' || echo "UNKNOWN")
+ fi
+ fi
+ # Fallback: npm view
+ if [[ "$license" == "UNKNOWN" ]] && command -v npm &>/dev/null; then
+ license=$(timeout 5 npm view "$pkg" license 2>/dev/null || echo "UNKNOWN")
+ fi
+ ;;
+ pip)
+ # Primary: pip show
+ if command -v pip &>/dev/null; then
+ license=$(timeout 5 pip show "$pkg" 2>/dev/null | grep -i '^License:' | sed 's/^[Ll]icense:[[:space:]]*//' || echo "UNKNOWN")
+ elif command -v pip3 &>/dev/null; then
+ license=$(timeout 5 pip3 show "$pkg" 2>/dev/null | grep -i '^License:' | sed 's/^[Ll]icense:[[:space:]]*//' || echo "UNKNOWN")
+ fi
+ ;;
+ go)
+ # Check module cache for LICENSE file
+ local gopath="${GOPATH:-$HOME/go}"
+ local mod_dir="$gopath/pkg/mod/$pkg"
+ # Try to find the latest version directory
+ if [[ -d "$gopath/pkg/mod" ]]; then
+ local found_dir
+ found_dir=$(find "$gopath/pkg/mod" -maxdepth 4 -path "*${pkg}@*" -type d 2>/dev/null | head -1)
+ if [[ -n "$found_dir" ]]; then
+ local lic_file
+ lic_file=$(find "$found_dir" -maxdepth 1 -iname 'LICENSE*' -type f 2>/dev/null | head -1)
+ if [[ -n "$lic_file" ]]; then
+ # Keyword match against common license identifiers
+ if grep -qiE 'GNU GENERAL PUBLIC LICENSE' "$lic_file" 2>/dev/null; then
+ if grep -qiE 'Version 3' "$lic_file" 2>/dev/null; then
+ license="GPL-3.0"
+ elif grep -qiE 'Version 2' "$lic_file" 2>/dev/null; then
+ license="GPL-2.0"
+ else
+ license="GPL"
+ fi
+ elif grep -qiE 'GNU LESSER GENERAL PUBLIC' "$lic_file" 2>/dev/null; then
+ license="LGPL"
+ elif grep -qiE 'GNU AFFERO GENERAL PUBLIC' "$lic_file" 2>/dev/null; then
+ license="AGPL-3.0"
+ elif grep -qiE 'MIT License' "$lic_file" 2>/dev/null; then
+ license="MIT"
+ elif grep -qiE 'Apache License' "$lic_file" 2>/dev/null; then
+ license="Apache-2.0"
+ elif grep -qiE 'BSD' "$lic_file" 2>/dev/null; then
+ license="BSD"
+ fi
+ fi
+ fi
+ fi
+ ;;
+ ruby)
+ # gem spec
+ if command -v gem &>/dev/null; then
+ license=$(timeout 5 gem spec "$pkg" license 2>/dev/null | grep -v '^---' | grep -v '^\.\.\.' | sed 's/^- //' | head -1 || echo "UNKNOWN")
+ [[ -z "$license" ]] && license="UNKNOWN"
+ fi
+ ;;
+ rust)
+ # cargo metadata
+ if command -v cargo &>/dev/null; then
+ if command -v jq &>/dev/null; then
+ license=$(timeout 5 cargo metadata --format-version 1 2>/dev/null | jq -r ".packages[] | select(.name == \"$pkg\") | .license // \"UNKNOWN\"" 2>/dev/null | head -1 || echo "UNKNOWN")
+ fi
+ fi
+ ;;
+ esac
+
+ # Normalize empty / whitespace-only to UNKNOWN
+ license=$(printf '%s' "$license" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+ [[ -z "$license" ]] && license="UNKNOWN"
+
+ printf '%s' "$license"
+}
+
+for dep in "${NEW_DEPS[@]}"; do
+ ecosystem="${dep%%:*}"
+ pkg="${dep#*:}"
+
+ license=$(get_license "$ecosystem" "$pkg")
+ RESULTS+=("$ecosystem $pkg $license")
+done
+
+# ---------------------------------------------------------------------------
+# Phase 3 & 4: Check against blocked list and allowlist
+# ---------------------------------------------------------------------------
+VIOLATIONS=()
+
+for result in "${RESULTS[@]}"; do
+ IFS=$'\t' read -r ecosystem pkg license <<< "$result"
+
+ # Phase 4: Skip allowlisted packages
+ if [[ ${#ALLOWLIST[@]} -gt 0 ]] && is_allowlisted "$pkg"; then
+ continue
+ fi
+
+ # Phase 3: Check against blocked list
+ if is_blocked_license "$license"; then
+ VIOLATIONS+=("$pkg $ecosystem $license BLOCKED")
+ FINDING_COUNT=$((FINDING_COUNT + 1))
+ fi
+done
+
+# ---------------------------------------------------------------------------
+# Phase 5: Output & logging
+# ---------------------------------------------------------------------------
+echo ""
+printf " %-30s %-12s %-30s %s\n" "PACKAGE" "ECOSYSTEM" "LICENSE" "STATUS"
+printf " %-30s %-12s %-30s %s\n" "-------" "---------" "-------" "------"
+
+for result in "${RESULTS[@]}"; do
+ IFS=$'\t' read -r ecosystem pkg license <<< "$result"
+
+ status="OK"
+ if [[ ${#ALLOWLIST[@]} -gt 0 ]] && is_allowlisted "$pkg"; then
+ status="ALLOWLISTED"
+ elif is_blocked_license "$license"; then
+ status="BLOCKED"
+ fi
+
+ printf " %-30s %-12s %-30s %s\n" "$pkg" "$ecosystem" "$license" "$status"
+done
+
+echo ""
+
+# Build JSON findings array
+FINDINGS_JSON="["
+FIRST=true
+for violation in "${VIOLATIONS[@]}"; do
+ IFS=$'\t' read -r pkg ecosystem license status <<< "$violation"
+ if [[ "$FIRST" != "true" ]]; then
+ FINDINGS_JSON+=","
+ fi
+ FIRST=false
+ FINDINGS_JSON+="{\"package\":\"$(json_escape "$pkg")\",\"ecosystem\":\"$(json_escape "$ecosystem")\",\"license\":\"$(json_escape "$license")\",\"status\":\"$(json_escape "$status")\"}"
+done
+FINDINGS_JSON+="]"
+
+# Write structured log entry
+printf '{"timestamp":"%s","event":"license_check_complete","mode":"%s","dependencies_checked":%d,"violation_count":%d,"violations":%s}\n' \
+ "$TIMESTAMP" "$MODE" "${#RESULTS[@]}" "$FINDING_COUNT" "$FINDINGS_JSON" >> "$LOG_FILE"
+
+if [[ $FINDING_COUNT -gt 0 ]]; then
+ echo "⚠️ Found $FINDING_COUNT license violation(s):"
+ echo ""
+ for violation in "${VIOLATIONS[@]}"; do
+ IFS=$'\t' read -r pkg ecosystem license status <<< "$violation"
+ echo " - $pkg ($ecosystem): $license"
+ done
+ echo ""
+
+ if [[ "$MODE" == "block" ]]; then
+ echo "🚫 Session blocked: resolve license violations above before committing."
+ echo " Set LICENSE_MODE=warn to log without blocking, or add packages to LICENSE_ALLOWLIST."
+ exit 1
+ else
+ echo "💡 Review the violations above. Set LICENSE_MODE=block to prevent commits with license issues."
+ fi
+else
+ echo "✅ All ${#RESULTS[@]} dependencies have compliant licenses"
+fi
+
+exit 0
diff --git a/hooks/dependency-license-checker/hooks.json b/hooks/dependency-license-checker/hooks.json
new file mode 100644
index 000000000..f1371b841
--- /dev/null
+++ b/hooks/dependency-license-checker/hooks.json
@@ -0,0 +1,16 @@
+{
+ "version": 1,
+ "hooks": {
+ "sessionEnd": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/dependency-license-checker/check-licenses.sh",
+ "cwd": ".",
+ "env": {
+ "LICENSE_MODE": "warn"
+ },
+ "timeoutSec": 60
+ }
+ ]
+ }
+}