New section for org-level Claude Code governance — fills the gap between individual dev security (security-hardening.md) and what engineering managers actually need when deploying at scale. New files: - guide/security/enterprise-governance.md (1117 lines) 6 sections: local/shared split, usage charter, MCP approval workflow, 4 guardrail tiers (Starter/Standard/Strict/Regulated), policy enforcement at scale, SOC2/ISO27001 compliance guide - examples/scripts/mcp-registry-template.yaml Org-level MCP registry with approved/pending/denied tracking - examples/hooks/bash/governance-enforcement-hook.sh SessionStart hook validating MCPs against approved registry - examples/scripts/ai-usage-charter-template.md Full charter template with data classification, use case rules, compliance mapping (SOC2/ISO27001/HIPAA/PCI DSS/GDPR) Enriched sections: - adoption-approaches.md: enterprise rollout (50+ devs) with 3-phase approach and common mistakes - observability.md: manager audit checklist, compliance reporting - ai-traceability.md: evidence collection table for auditors - production-safety.md + security-hardening.md: cross-references with explicit scope boundaries Integration: guide/README.md, reference.yaml (22 new entries), CHANGELOG.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
6.6 KiB
Bash
Executable file
177 lines
6.6 KiB
Bash
Executable file
#!/bin/bash
|
|
# governance-enforcement-hook.sh
|
|
# Event: SessionStart
|
|
#
|
|
# Validates active Claude Code MCP configuration against the org's approved registry.
|
|
# Warns about unapproved MCPs and checks that security deny rules are in place.
|
|
#
|
|
# Related: guide/security/enterprise-governance.md §3.3 and §5
|
|
#
|
|
# Installation:
|
|
# cp governance-enforcement-hook.sh ~/.claude/hooks/
|
|
# chmod +x ~/.claude/hooks/governance-enforcement-hook.sh
|
|
#
|
|
# Then add to ~/.claude/settings.json:
|
|
# {
|
|
# "hooks": {
|
|
# "SessionStart": ["~/.claude/hooks/governance-enforcement-hook.sh"]
|
|
# }
|
|
# }
|
|
#
|
|
# Or in project .claude/settings.json:
|
|
# {
|
|
# "hooks": {
|
|
# "SessionStart": [".claude/hooks/governance-enforcement-hook.sh"]
|
|
# }
|
|
# }
|
|
|
|
set -euo pipefail
|
|
|
|
SETTINGS_FILE="${HOME}/.claude.json"
|
|
REGISTRY_FILE="${PWD}/.claude/mcp-registry.yaml"
|
|
PROJECT_SETTINGS="${PWD}/.claude/settings.json"
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Helpers
|
|
# ─────────────────────────────────────────────────────────────────
|
|
|
|
warn() { echo "GOVERNANCE WARNING: $*" >&2; }
|
|
info() { echo "GOVERNANCE: $*" >&2; }
|
|
|
|
has_command() { command -v "$1" &>/dev/null; }
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Check 1: Project governance config present
|
|
# ─────────────────────────────────────────────────────────────────
|
|
|
|
check_project_config() {
|
|
if [[ ! -f "$PROJECT_SETTINGS" ]]; then
|
|
warn "No .claude/settings.json in project. Governance not applied to this repo."
|
|
warn "Set up governance: see guide/security/enterprise-governance.md"
|
|
return
|
|
fi
|
|
|
|
# Check that deny rules exist for secrets
|
|
if has_command jq; then
|
|
local has_deny
|
|
has_deny=$(jq -e '.permissions.deny // [] | length' "$PROJECT_SETTINGS" 2>/dev/null || echo "0")
|
|
if [[ "$has_deny" == "0" ]]; then
|
|
warn "No permissions.deny rules in .claude/settings.json."
|
|
warn "Add deny rules for .env, *.key, *.pem files."
|
|
fi
|
|
|
|
# Check for dangerous allow rules
|
|
local dangerous_allows
|
|
dangerous_allows=$(jq -r '.permissions.allow[]? // ""' "$PROJECT_SETTINGS" 2>/dev/null | \
|
|
grep -iE '(env|secret|credential|password|token|key)' || true)
|
|
if [[ -n "$dangerous_allows" ]]; then
|
|
warn "DANGEROUS: permissions.allow contains sensitive patterns:"
|
|
echo "$dangerous_allows" | while read -r line; do
|
|
warn " Allowed: $line"
|
|
done
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Check 2: MCP registry compliance
|
|
# ─────────────────────────────────────────────────────────────────
|
|
|
|
check_mcp_registry() {
|
|
if [[ ! -f "$REGISTRY_FILE" ]]; then
|
|
# No registry = no enforcement (opt-in)
|
|
return
|
|
fi
|
|
|
|
if ! has_command jq || ! has_command yq; then
|
|
warn "jq or yq not installed — cannot check MCP registry compliance."
|
|
warn "Install: brew install jq yq"
|
|
return
|
|
fi
|
|
|
|
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
return
|
|
fi
|
|
|
|
# Get active MCPs
|
|
local active_mcps
|
|
active_mcps=$(jq -r '.mcpServers // {} | keys[]' "$SETTINGS_FILE" 2>/dev/null || true)
|
|
|
|
if [[ -z "$active_mcps" ]]; then
|
|
return
|
|
fi
|
|
|
|
# Get approved MCPs from registry
|
|
local approved_mcps
|
|
approved_mcps=$(yq e '.approved[].name' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
|
|
# Compare
|
|
local unapproved=()
|
|
while IFS= read -r mcp; do
|
|
if [[ -n "$mcp" ]] && ! echo "$approved_mcps" | grep -q "^${mcp}$"; then
|
|
unapproved+=("$mcp")
|
|
fi
|
|
done <<< "$active_mcps"
|
|
|
|
if [[ ${#unapproved[@]} -gt 0 ]]; then
|
|
warn "Unapproved MCP servers detected:"
|
|
for mcp in "${unapproved[@]}"; do
|
|
warn " - $mcp (not in .claude/mcp-registry.yaml)"
|
|
done
|
|
warn "Submit approval request per your org's AI usage charter."
|
|
warn "Session continues — remediate within 48 hours per policy."
|
|
else
|
|
info "MCP registry check passed. All MCPs approved."
|
|
fi
|
|
|
|
# Check for expired approvals
|
|
local today
|
|
today=$(date +%Y-%m-%d)
|
|
while IFS= read -r name; do
|
|
local expires
|
|
expires=$(yq e ".approved[] | select(.name == \"$name\") | .expires" "$REGISTRY_FILE" 2>/dev/null || true)
|
|
if [[ -n "$expires" && "$expires" < "$today" ]]; then
|
|
warn "MCP '$name' approval expired on $expires. Re-approval required."
|
|
fi
|
|
done <<< "$approved_mcps"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Check 3: Data classification context
|
|
# ─────────────────────────────────────────────────────────────────
|
|
|
|
check_data_classification() {
|
|
# Detect sensitive files in working directory
|
|
local sensitive_patterns=(".env" "*.pem" "*.key" "secrets/" "credentials")
|
|
local found_sensitive=()
|
|
|
|
for pattern in "${sensitive_patterns[@]}"; do
|
|
if compgen -G "${PWD}/${pattern}" &>/dev/null 2>/dev/null; then
|
|
found_sensitive+=("$pattern")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#found_sensitive[@]} -gt 0 ]]; then
|
|
info "Sensitive file patterns detected in working directory:"
|
|
for f in "${found_sensitive[@]}"; do
|
|
info " - $f"
|
|
done
|
|
info "Verify .claude/settings.json deny rules cover these files."
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Main
|
|
# ─────────────────────────────────────────────────────────────────
|
|
|
|
main() {
|
|
check_project_config
|
|
check_mcp_registry
|
|
check_data_classification
|
|
|
|
# Always exit 0 — governance hook warns but does not block session start
|
|
# Blocking at SessionStart creates too much friction; use periodic audits instead
|
|
exit 0
|
|
}
|
|
|
|
main
|