Skill: suhteevah/envguard (version 1.0.1)

--- name: envguard description: Pre-commit secret detection — block leaked credentials, API keys, and .env files before they hit git homepage: https://envguard.pages.dev metadata: { "openclaw": { "emoji": "\ud83d\udd10", "primaryEnv": "ENVGUARD_LICENSE_KEY", "requires": { "bins": ["git", "bash", "python3", "jq"] }, "configPaths": ["~/.openclaw/openclaw.json"], "install": [ { "id": "lefthook", "kind": "brew", "formula": "lefthook", "bins": ["lefthook"], "label": "Install lefthook (git hooks manager)" } ], "os": ["darwin", "linux", "win32"] } } user-invocable: true disable-model-invocation: false --- # EnvGuard — Pre-Commit Secret Detection EnvGuard scans your code for leaked secrets, credentials, API keys, and .env file contents before they reach git. It uses regex-based pattern matching against 50+ secret formats from 20+ services, lefthook for git hook integration, and produces SARIF-compatible reports for compliance workflows. ## Commands ### Free Tier (No license required) #### `envguard scan [file|directory]` One-shot secret scan of files or directories. **How to execute:** ```bash bash "<SKILL_DIR>/scripts/envguard.sh" scan [target] ``` **What it does:** 1. Accepts a file path or directory (defaults to current directory) 2. Finds all text files (excluding .git/, node_modules/, dist/, build/, vendor/, __pycache__) 3. Runs 50+ secret detection patterns against each file 4. Respects .envguardignore exclusions (gitignore syntax) 5. Outputs findings with: file, line number, pattern matched, severity, redacted match 6. Exit code 0 if clean, exit code 1 if critical/high findings detected **Example usage scenarios:** - "Scan this repo for leaked secrets" -> runs `envguard scan .` - "Check this file for API keys" -> runs `envguard scan src/config.ts` - "Are there any secrets in my source code?" -> runs `envguard scan src/` ### Pro Tier ($19/user/month -- requires ENVGUARD_LICENSE_KEY) #### `envguard hooks install` Install git pre-commit hooks that scan staged files for secrets before every commit. **How to execute:** ```bash bash "<SKILL_DIR>/scripts/envguard.sh" hooks install ``` **What it does:** 1. Validates Pro+ license 2. Copies lefthook config to project root 3. Installs lefthook pre-commit hook 4. On every commit: scans all staged files for secrets, blocks commit if secrets found, shows remediation advice #### `envguard hooks uninstall` Remove EnvGuard git hooks. ```bash bash "<SKILL_DIR>/scripts/envguard.sh" hooks uninstall ``` #### `envguard allowlist [add|remove|list] [pattern]` Manage false positive patterns. Allowlisted patterns are skipped during scanning. ```bash bash "<SKILL_DIR>/scripts/envguard.sh" allowlist add "EXAMPLE_API_KEY_FOR_TESTS" bash "<SKILL_DIR>/scripts/envguard.sh" allowlist remove "EXAMPLE_API_KEY_FOR_TESTS" bash "<SKILL_DIR>/scripts/envguard.sh" allowlist list ``` **What it does:** 1. Validates Pro+ license 2. Reads/writes allowlist in ~/.openclaw/openclaw.json (envguard.config.allowlist) 3. Allowlisted patterns are treated as known-safe and skipped during scans #### `envguard diff` Scan only staged changes (git diff --cached) for secrets. ```bash bash "<SKILL_DIR>/scripts/envguard.sh" diff ``` **What it does:** 1. Validates Pro+ license 2. Gets staged changes via `git diff --cached` 3. Scans only added/modified lines for secrets 4. Ideal for pre-commit checks on large repos ### Team Tier ($39/user/month -- requires ENVGUARD_LICENSE_KEY with team tier) #### `envguard history [directory]` Full git history scan -- finds secrets in all previous commits. ```bash bash "<SKILL_DIR>/scripts/envguard.sh" history [directory] ``` **What it does:** 1. Validates Team+ license 2. Walks entire git log using `git log -p` 3. Scans every diff for secrets across all commits 4. Reports: commit hash, author, date, file, line, pattern matched 5. Critical for onboarding repos that may have had secrets committed in the past #### `envguard report [directory]` Generate a SARIF-compatible or markdown compliance report. ```bash bash "<SKILL_DIR>/scripts/envguard.sh" report [directory] ``` **What it does:** 1. Validates Team+ license 2. Runs full scan of the directory 3. Generates a formatted markdown report with severity breakdown 4. Includes remediation steps for each finding category 5. Output suitable for compliance audits and security reviews #### `envguard policy [directory]` Custom secret patterns and enforcement rules. ```bash bash "<SKILL_DIR>/scripts/envguard.sh" policy [directory] ``` **What it does:** 1. Validates Team+ license 2. Loads custom patterns from ~/.openclaw/openclaw.json (envguard.config.customPatterns) 3. Enforces organization-specific secret rules (e.g., internal token formats) 4. Combines custom patterns with built-in patterns for comprehensive scanning ## Detected Secret Types EnvGuard detects 50+ secret patterns across 20+ services: | Category | Examples | Severity | |----------|----------|----------| | AWS Credentials | AKIA* keys, aws_secret_access_key | Critical | | Stripe Keys | sk_live_*, sk_test_*, rk_live_*, whsec_* | Critical | | GitHub Tokens | ghp_*, gho_*, ghu_*, ghs_*, ghr_* | Critical | | GitLab Tokens | glpat-* | Critical | | Private Keys | RSA, OPENSSH, DSA, EC, PGP private keys | Critical | | Slack Tokens | xoxb-*, xoxp-*, xoxo-*, xapp-* | High | | Google API Keys | AIza* | High | | JWT Tokens | eyJ* (long base64 tokens) | High | | Database URIs | postgres://, mysql://, mongodb://, redis:// | High | | Twilio Keys | SK* account SIDs | High | | SendGrid Keys | SG.* | High | | Firebase/Supabase | API keys and service tokens | High | | npm Tokens | npm_* | High | | Heroku API Keys | Heroku token patterns | Medium | | DigitalOcean | dop_v1_*, doo_v1_* | Medium | | Azure Keys | Azure subscription/account keys | Medium | | Cloudflare | API tokens and keys | Medium | | Docker Hub | Docker auth tokens | Medium | | Mailgun/Postmark | API keys | Medium | | Generic Secrets | api_key=, password=, secret=, token= | Low | | .env Leaks | KEY=value patterns in source files | Low | ## Configuration Users can configure EnvGuard in `~/.openclaw/openclaw.json`: ```json { "skills": { "entries": { "envguard": { "enabled": true, "apiKey": "YOUR_LICENSE_KEY_HERE", "config": { "severityThreshold": "high", "allowlist": [], "customPatterns": [], "excludePatterns": ["**/node_modules/**", "**/dist/**", "**/.git/**"], "reportFormat": "markdown" } } } } } ``` ## Important Notes - **Free tier** works immediately with no configuration - **All scanning happens locally** -- no code or secrets are sent to external servers - **License validation is offline** -- no phone-home or network calls - Supports .envguardignore files (gitignore syntax) to exclude paths - Matches are always **redacted** in output (first/last 4 chars only) - Git hooks use **lefthook** which must be installed (see install metadata above) - Exit codes: 0 = clean, 1 = findings detected (for CI/CD integration) ## Error Handling - If lefthook is not installed and user tries `hooks install`, prompt to install it - If license key is invalid or expired, show clear message with link to https://envguard.pages.dev/renew - If a file is binary, skip it automatically with no warning - If .envguardignore is malformed, warn and continue with default excludes - If no files found in target, report clean scan with info message ## When to Use EnvGuard The user might say things like: - "Scan for leaked secrets" - "Check if any API keys are in my code" - "Set up secret scanning on my commits" - "Are there any credentials in this repo?" - "Generate a security report for compliance" - "Scan git history for leaked passwords" - "Block secrets from being committed" - "Check my staged files for secrets" - "Add a false positive to the allowlist"

path: config/lefthook.yml
content: # EnvGuard — lefthook configuration
# Installed by: envguard hooks install
# Documentation: https://envguard.pages.dev/docs/hooks

pre-commit:
  parallel: true
  commands:
    envguard-secret-scan:
      run: |
        ENVGUARD_SKILL_DIR="${ENVGUARD_SKILL_DIR:-$HOME/.openclaw/skills/envguard}"
        if [[ -f "$ENVGUARD_SKILL_DIR/scripts/scanner.sh" ]]; then
          source "$ENVGUARD_SKILL_DIR/scripts/scanner.sh"
          hook_secret_scan
        else
          echo "[EnvGuard] Skill not found at $ENVGUARD_SKILL_DIR — skipping scan"
        fi
      fail_text: |
        Secrets detected in staged files!
        Run 'envguard scan' to see details.
        Run 'envguard allowlist add <pattern>' to allow known-safe patterns.
        Or 'git commit --no-verify' to skip (NOT recommended).


path: README.md
content: # envguard

<p align="center">
  <img src="https://img.shields.io/badge/patterns-50+-blue" alt="50+ patterns">
  <img src="https://img.shields.io/badge/services-20+-purple" alt="20+ services">
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License">
  <img src="https://img.shields.io/badge/install-clawhub-blue" alt="ClawHub">
  <img src="https://img.shields.io/badge/zero-telemetry-brightgreen" alt="Zero telemetry">
</p>

<h3 align="center">Pre-commit secret detection. Block leaked credentials before they hit git.</h3>

<p align="center">
  <a href="https://envguard.pages.dev">Website</a> &middot;
  <a href="#quick-start">Quick Start</a> &middot;
  <a href="#supported-patterns">Patterns</a> &middot;
  <a href="https://envguard.pages.dev/#pricing">Pricing</a>
</p>

---

## Your .env just got committed. Again.

It happens to everyone. A Stripe key in a config file. An AWS secret in a test fixture. A database URL in a docker-compose checked into a public repo.

GitGuardian emails you 3 hours later. By then, bots have already scraped it.

**EnvGuard catches secrets before they leave your machine.** Pre-commit hooks. Local scanning. 50+ patterns. Zero data leaves your laptop.

## Quick Start

```bash
# 1. Install via ClawHub (free)
clawhub install envguard

# 2. Scan your repo
envguard scan

# 3. Install pre-commit hooks (Pro)
envguard hooks install
```

That's it. Every commit is now scanned for secrets.

## What It Does

### Scan files for secrets
One command to scan any file, directory, or your entire repo. 50+ regex patterns detect API keys, tokens, passwords, private keys, and connection strings from 20+ services.

### Block commits with pre-commit hooks
Install a lefthook pre-commit hook that scans every staged file. If a secret is detected, the commit is blocked with a clear remediation message.

### Manage false positives
Allowlist known-safe patterns (test fixtures, example values) so they don't trigger on every scan.

### Scan staged changes only
For large repos, scan only the diff instead of the entire codebase. Faster, focused, and perfect for CI.

### Full git history scan
Audit your entire git history for secrets that were committed in the past. Even deleted secrets remain in git objects -- find them before an attacker does.

### Generate compliance reports
Produce markdown reports with severity breakdowns and remediation steps. Ideal for security reviews and compliance audits.

### Custom patterns
Define organization-specific secret patterns (internal tokens, proprietary formats) alongside the built-in 50+ patterns.

## How It Compares

| Feature | EnvGuard | GitGuardian | Gitleaks | TruffleHog |
|---------|:--------:|:-----------:|:--------:|:----------:|
| Local-only (no cloud) | Yes | No | Yes | Yes |
| Pre-commit hooks | Yes | Yes | Yes | Yes |
| Zero config scan | Yes | No | Config required | Config required |
| Custom patterns | Yes | Enterprise | Yes | Yes |
| Git history scan | Yes | Yes | Yes | Yes |
| Allowlist management | Yes | Yes | Via config | Via config |
| SARIF-compatible reports | Yes | Yes | Yes | Yes |
| Offline license validation | Yes | N/A | N/A | N/A |
| Zero telemetry | Yes | No | Yes | Yes |
| ClawHub integration | Yes | No | No | No |
| Price (individual) | Free/$19/mo | $0-$380/mo | Free | Free |

## Supported Patterns

EnvGuard detects 50+ secret patterns across 20+ services:

| Service | Patterns | Severity |
|---------|----------|----------|
| **AWS** | Access Key IDs (AKIA), Secret Access Keys, STS tokens | Critical |
| **Stripe** | sk_live, sk_test, rk_live, rk_test, whsec_ | Critical |
| **GitHub** | ghp_, gho_, ghu_, ghs_, ghr_, github_pat_ | Critical |
| **GitLab** | glpat-, gloas-, glrt- | Critical |
| **Private Keys** | RSA, OpenSSH, DSA, EC, PGP, PKCS8 | Critical |
| **Slack** | xoxb-, xoxp-, xoxo-, xapp-, xoxs- | High |
| **Google** | AIza API keys, OAuth client IDs | High |
| **JWT** | eyJ... tokens | High |
| **Database** | postgres://, mysql://, mongodb://, redis://, amqp://, mssql:// | High |
| **Twilio** | SK (API Keys), AC (Account SIDs) | High |
| **SendGrid** | SG.* API keys | High |
| **npm** | npm_ tokens, registry auth | High |
| **Firebase** | API keys, config values | High |
| **Supabase** | Service role keys, JWTs | High |
| **Vercel** | vercel_ tokens | High |
| **Heroku** | API keys (UUID format) | Medium |
| **DigitalOcean** | dop_v1_, doo_v1_ | Medium |
| **Azure** | AccountKey, client secrets, SAS tokens | Medium |
| **Cloudflare** | API tokens, global API keys | Medium |
| **Docker** | dckr_pat_ tokens, password configs | Medium |
| **Mailgun** | key- API keys | Medium |
| **Generic** | api_key=, secret=, password=, token= | Low |
| **.env leaks** | KEY=value patterns in source files | Low |

## Pricing

| Feature | Free | Pro ($19/user/mo) | Team ($39/user/mo) |
|---------|:----:|:------------------:|:-------------------:|
| One-shot secret scan | Yes | Yes | Yes |
| 50+ detection patterns | Yes | Yes | Yes |
| .envguardignore support | Yes | Yes | Yes |
| Pre-commit hooks | | Yes | Yes |
| Allowlist management | | Yes | Yes |
| Staged diff scanning | | Yes | Yes |
| Full git history scan | | | Yes |
| Compliance reports | | | Yes |
| Custom patterns | | | Yes |

## Configuration

Add to `~/.openclaw/openclaw.json`:

```json
{
  "skills": {
    "entries": {
      "envguard": {
        "enabled": true,
        "apiKey": "YOUR_LICENSE_KEY",
        "config": {
          "severityThreshold": "high",
          "allowlist": ["EXAMPLE_KEY_FOR_TESTS"],
          "customPatterns": [
            {
              "regex": "myco_[0-9a-f]{32}",
              "severity": "critical",
              "service": "MyCorp",
              "description": "MyCorp internal API token"
            }
          ],
          "excludePatterns": ["**/test-fixtures/**"]
        }
      }
    }
  }
}
```

## .envguardignore

Create a `.envguardignore` file in your repo root to exclude paths from scanning. Uses the same syntax as `.gitignore`:

```
# Test fixtures with fake secrets
tests/fixtures/**
test-data/**

# Generated files
*.min.js
dist/**

# Documentation with example keys
docs/examples/**
```

## Privacy

- 100% local -- no code or secrets sent externally
- Zero telemetry
- Offline license validation
- Matches are always redacted in output (first/last 4 chars only)

## License

MIT


path: scripts/envguard.sh
content: #!/usr/bin/env bash
# EnvGuard — Main CLI entry point
# Usage: envguard.sh <command> [args...]
#
# Commands:
#   scan [file|dir]           One-shot secret scan (FREE)
#   hooks install             Install pre-commit hooks (PRO+)
#   hooks uninstall           Remove EnvGuard hooks (PRO+)
#   allowlist [add|remove|list] Manage false positives (PRO+)
#   diff                      Scan staged changes only (PRO+)
#   history [dir]             Full git history scan (TEAM+)
#   report [dir]              Generate compliance report (TEAM+)
#   policy [dir]              Custom patterns + enforcement (TEAM+)
#   status                    Show license and config info

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
VERSION="1.0.0"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

log_info()    { echo -e "${BLUE}[EnvGuard]${NC} $*"; }
log_success() { echo -e "${GREEN}[EnvGuard]${NC} $*"; }
log_warn()    { echo -e "${YELLOW}[EnvGuard]${NC} $*"; }
log_error()   { echo -e "${RED}[EnvGuard]${NC} $*" >&2; }

show_version() {
  echo -e "${BOLD}EnvGuard${NC} v${VERSION}"
  echo "  Pre-commit secret detection"
  echo "  https://envguard.pages.dev"
}

show_help() {
  show_version
  echo ""
  echo -e "${BOLD}USAGE${NC}"
  echo "  envguard <command> [options]"
  echo ""
  echo -e "${BOLD}FREE COMMANDS${NC}"
  echo -e "  scan [file|dir]           ${DIM}[FREE]${NC} One-shot secret scan"
  echo ""
  echo -e "${BOLD}PRO COMMANDS${NC} (\$19/user/month)"
  echo -e "  hooks install             ${DIM}[PRO]${NC}  Install pre-commit hooks"
  echo -e "  hooks uninstall           ${DIM}[PRO]${NC}  Remove EnvGuard hooks"
  echo -e "  allowlist add <pattern>   ${DIM}[PRO]${NC}  Allow a known-safe pattern"
  echo -e "  allowlist remove <pat>    ${DIM}[PRO]${NC}  Remove from allowlist"
  echo -e "  allowlist list            ${DIM}[PRO]${NC}  Show allowlisted patterns"
  echo -e "  diff                      ${DIM}[PRO]${NC}  Scan staged changes only"
  echo ""
  echo -e "${BOLD}TEAM COMMANDS${NC} (\$39/user/month)"
  echo -e "  history [dir]             ${DIM}[TEAM]${NC} Full git history scan"
  echo -e "  report [dir]              ${DIM}[TEAM]${NC} Generate compliance report"
  echo -e "  policy [dir]              ${DIM}[TEAM]${NC} Custom patterns + enforcement"
  echo ""
  echo -e "${BOLD}OTHER${NC}"
  echo "  status                    Show license and config info"
  echo "  patterns                  List all detection patterns"
  echo "  --help, -h                Show this help"
  echo "  --version, -v             Show version"
  echo ""
  echo -e "Get a license at ${CYAN}https://envguard.pages.dev${NC}"
}

# ─── License ────────────────────────────────────────────────────────────────

require_license() {
  local required_tier="${1:-pro}"
  source "$SCRIPT_DIR/license.sh"
  check_envguard_license "$required_tier"
}

# ─── Allowlist management ──────────────────────────────────────────────────

OPENCLAW_CONFIG="${HOME}/.openclaw/openclaw.json"

do_allowlist() {
  local subcmd="${1:-list}"
  local pattern="${2:-}"

  case "$subcmd" in
    add)
      if [[ -z "$pattern" ]]; then
        log_error "Usage: envguard allowlist add <pattern>"
        return 1
      fi

      # Ensure config directory exists
      mkdir -p "$(dirname "$OPENCLAW_CONFIG")"

      if command -v python3 &>/dev/null; then
        python3 -c "
import json, os

config_path = os.path.expanduser('$OPENCLAW_CONFIG')
cfg = {}
if os.path.exists(config_path):
    with open(config_path) as f:
        cfg = json.load(f)

# Navigate/create nested structure
cfg.setdefault('skills', {}).setdefault('entries', {}).setdefault('envguard', {}).setdefault('config', {}).setdefault('allowlist', [])

allowlist = cfg['skills']['entries']['envguard']['config']['allowlist']
pattern = '$pattern'
if pattern not in allowlist:
    allowlist.append(pattern)

with open(config_path, 'w') as f:
    json.dump(cfg, f, indent=2)
print('added')
" 2>/dev/null
        log_success "Added to allowlist: ${BOLD}$pattern${NC}"
      elif command -v node &>/dev/null; then
        node -e "
const fs = require('fs');
const path = '$OPENCLAW_CONFIG';
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(path, 'utf8')); } catch(e) {}
if (!cfg.skills) cfg.skills = {};
if (!cfg.skills.entries) cfg.skills.entries = {};
if (!cfg.skills.entries.envguard) cfg.skills.entries.envguard = {};
if (!cfg.skills.entries.envguard.config) cfg.skills.entries.envguard.config = {};
if (!cfg.skills.entries.envguard.config.allowlist) cfg.skills.entries.envguard.config.allowlist = [];
const al = cfg.skills.entries.envguard.config.allowlist;
if (!al.includes('$pattern')) al.push('$pattern');
fs.writeFileSync(path, JSON.stringify(cfg, null, 2));
" 2>/dev/null
        log_success "Added to allowlist: ${BOLD}$pattern${NC}"
      else
        log_error "Requires python3 or node to manage allowlist."
        return 1
      fi
      ;;

    remove)
      if [[ -z "$pattern" ]]; then
        log_error "Usage: envguard allowlist remove <pattern>"
        return 1
      fi

      if [[ ! -f "$OPENCLAW_CONFIG" ]]; then
        log_warn "No config file found. Allowlist is empty."
        return 0
      fi

      if command -v python3 &>/dev/null; then
        python3 -c "
import json, os

config_path = os.path.expanduser('$OPENCLAW_CONFIG')
if not os.path.exists(config_path):
    print('not_found')
    exit()

with open(config_path) as f:
    cfg = json.load(f)

try:
    allowlist = cfg['skills']['entries']['envguard']['config']['allowlist']
    pattern = '$pattern'
    if pattern in allowlist:
        allowlist.remove(pattern)
        with open(config_path, 'w') as f:
            json.dump(cfg, f, indent=2)
        print('removed')
    else:
        print('not_found')
except (KeyError, TypeError):
    print('not_found')
" 2>/dev/null | {
          read -r result
          if [[ "$result" == "removed" ]]; then
            log_success "Removed from allowlist: ${BOLD}$pattern${NC}"
          else
            log_warn "Pattern not found in allowlist: $pattern"
          fi
        }
      elif command -v node &>/dev/null; then
        node -e "
const fs = require('fs');
const path = '$OPENCLAW_CONFIG';
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(path, 'utf8')); } catch(e) {}
const al = cfg?.skills?.entries?.envguard?.config?.allowlist || [];
const idx = al.indexOf('$pattern');
if (idx >= 0) {
  al.splice(idx, 1);
  fs.writeFileSync(path, JSON.stringify(cfg, null, 2));
  console.log('removed');
} else {
  console.log('not_found');
}
" 2>/dev/null | {
          read -r result
          if [[ "$result" == "removed" ]]; then
            log_success "Removed from allowlist: ${BOLD}$pattern${NC}"
          else
            log_warn "Pattern not found in allowlist: $pattern"
          fi
        }
      else
        log_error "Requires python3 or node to manage allowlist."
        return 1
      fi
      ;;

    list)
      echo -e "${BOLD}━━━ EnvGuard Allowlist ━━━${NC}"
      echo ""

      local items=""
      if [[ -f "$OPENCLAW_CONFIG" ]]; then
        if command -v python3 &>/dev/null; then
          items=$(python3 -c "
import json
try:
    with open('$OPENCLAW_CONFIG') as f:
        cfg = json.load(f)
    for item in cfg.get('skills', {}).get('entries', {}).get('envguard', {}).get('config', {}).get('allowlist', []):
        print(item)
except: pass
" 2>/dev/null) || true
        elif command -v node &>/dev/null; then
          items=$(node -e "
try {
  const cfg = require('$OPENCLAW_CONFIG');
  (cfg?.skills?.entries?.envguard?.config?.allowlist || []).forEach(i => console.log(i));
} catch(e) {}
" 2>/dev/null) || true
        elif command -v jq &>/dev/null; then
          items=$(jq -r '.skills.entries.envguard.config.allowlist[]? // empty' "$OPENCLAW_CONFIG" 2>/dev/null) || true
        fi
      fi

      if [[ -z "$items" ]]; then
        echo -e "  ${DIM}(empty)${NC}"
        echo ""
        echo -e "Add patterns: ${CYAN}envguard allowlist add <pattern>${NC}"
      else
        local count=0
        while IFS= read -r item; do
          [[ -z "$item" ]] && continue
          ((count++)) || true
          echo -e "  ${GREEN}*${NC} $item"
        done <<< "$items"
        echo ""
        echo -e "  ${DIM}$count pattern(s) allowlisted${NC}"
      fi
      ;;

    *)
      log_error "Unknown allowlist subcommand: $subcmd"
      echo "  Usage: envguard allowlist [add|remove|list] [pattern]"
      return 1
      ;;
  esac
}

# ─── Hooks management ──────────────────────────────────────────────────────

do_hooks_install() {
  if ! command -v lefthook &>/dev/null; then
    log_error "lefthook not installed."
    echo "  Install: brew install lefthook"
    return 1
  fi

  if ! git rev-parse --git-dir &>/dev/null 2>&1; then
    log_error "Not inside a git repository."
    return 1
  fi

  local repo_root
  repo_root=$(git rev-parse --show-toplevel)

  log_info "Installing hooks in ${BOLD}$repo_root${NC}"

  local config="$repo_root/lefthook.yml"

  if [[ -f "$config" ]]; then
    if grep -q "envguard" "$config" 2>/dev/null; then
      log_success "Hooks already configured."
      return 0
    fi

    # Append to existing config
    cat >> "$config" <<'HOOKS'

# ─── EnvGuard hooks ─────────────────────────────
    envguard-secret-scan:
      run: |
        ENVGUARD_SKILL_DIR="${ENVGUARD_SKILL_DIR:-$HOME/.openclaw/skills/envguard}"
        if [[ -f "$ENVGUARD_SKILL_DIR/scripts/scanner.sh" ]]; then
          source "$ENVGUARD_SKILL_DIR/scripts/scanner.sh"
          hook_secret_scan
        else
          echo "[EnvGuard] Skill not found at $ENVGUARD_SKILL_DIR — skipping scan"
        fi
      fail_text: |
        Secrets detected in staged files!
        Run 'envguard scan' to see details.
        Run 'envguard allowlist add <pattern>' to allow known-safe patterns.
        Or 'git commit --no-verify' to skip (NOT recommended).
HOOKS
    echo -e "${GREEN}+${NC} Appended EnvGuard hooks to existing lefthook.yml"
  else
    cp "$SKILL_DIR/config/lefthook.yml" "$config"
    echo -e "${GREEN}+${NC} Created lefthook.yml"
  fi

  (cd "$repo_root" && lefthook install)
  echo ""
  log_success "Hooks installed! Staged files will be scanned for secrets on every commit."
}

do_hooks_uninstall() {
  if ! git rev-parse --git-dir &>/dev/null 2>&1; then
    log_error "Not inside a git repository."
    return 1
  fi

  local repo_root
  repo_root=$(git rev-parse --show-toplevel)
  local config="$repo_root/lefthook.yml"

  if [[ -f "$config" ]]; then
    if grep -q "envguard" "$config" 2>/dev/null; then
      local tmp
      tmp=$(mktemp)
      sed '/# ─── EnvGuard hooks/,/fail_text:.*$/d' "$config" > "$tmp"
      grep -v "envguard" "$tmp" > "$config" 2>/dev/null || mv "$tmp" "$config"
      rm -f "$tmp"
      echo -e "${GREEN}+${NC} Removed EnvGuard hooks"
    else
      log_warn "No EnvGuard hooks found"
    fi
  else
    log_warn "No lefthook.yml found"
  fi
}

# ─── Pattern listing ───────────────────────────────────────────────────────

do_list_patterns() {
  source "$SCRIPT_DIR/patterns.sh"

  echo -e "${BOLD}━━━ EnvGuard Detection Patterns ━━━${NC}"
  echo ""
  echo -e "${BOLD}$(envguard_pattern_count) patterns across 20+ services${NC}"
  echo ""

  echo -e "${RED}${BOLD}CRITICAL:${NC}"
  envguard_list_patterns "critical" | while IFS= read -r line; do
    echo "  $line"
  done
  echo ""

  echo -e "${RED}HIGH:${NC}"
  envguard_list_patterns "high" | while IFS= read -r line; do
    echo "  $line"
  done
  echo ""

  echo -e "${YELLOW}MEDIUM:${NC}"
  envguard_list_patterns "medium" | while IFS= read -r line; do
    echo "  $line"
  done
  echo ""

  echo -e "${BLUE}LOW:${NC}"
  envguard_list_patterns "low" | while IFS= read -r line; do
    echo "  $line"
  done
}

# ─── Command dispatch ───────────────────────────────────────────────────────

main() {
  local cmd="${1:-}"
  shift || true

  case "$cmd" in
    scan)
      local target="${1:-.}"
      log_info "Scanning for secrets in ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/scanner.sh"
      do_secret_scan "$target"
      ;;

    hooks)
      require_license "pro"
      local subcmd="${1:-}"
      case "$subcmd" in
        install)
          do_hooks_install
          ;;
        uninstall)
          do_hooks_uninstall
          ;;
        *)
          log_error "Unknown hooks subcommand: $subcmd"
          echo "  Usage: envguard hooks [install|uninstall]"
          exit 1
          ;;
      esac
      ;;

    allowlist)
      require_license "pro"
      local subcmd="${1:-list}"
      shift || true
      local pattern="${1:-}"
      do_allowlist "$subcmd" "$pattern"
      ;;

    diff)
      require_license "pro"
      log_info "Scanning staged changes for secrets..."
      source "$SCRIPT_DIR/scanner.sh"
      do_diff_scan
      ;;

    history)
      require_license "team"
      local target="${1:-.}"
      log_info "Scanning git history in ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/scanner.sh"
      do_history_scan "$target"
      ;;

    report)
      require_license "team"
      local target="${1:-.}"
      log_info "Generating security report for ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/scanner.sh"
      do_secret_report "$target"
      ;;

    policy)
      require_license "team"
      local target="${1:-.}"
      log_info "Running policy enforcement in ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/scanner.sh"
      do_policy_scan "$target"
      ;;

    patterns)
      do_list_patterns
      ;;

    status)
      show_version
      echo ""
      source "$SCRIPT_DIR/license.sh"
      show_envguard_status
      ;;

    --help|-h|help)
      show_help
      ;;

    --version|-v)
      show_version
      ;;

    "")
      show_help
      exit 1
      ;;

    *)
      log_error "Unknown command: $cmd"
      echo ""
      show_help
      exit 1
      ;;
  esac
}

main "$@"


path: scripts/license.sh
content: #!/usr/bin/env bash
# EnvGuard — License Validation Module
# Same JWT-based offline validation as DocSync/DepGuard

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

OPENCLAW_CONFIG="${HOME}/.openclaw/openclaw.json"
ENVGUARD_LICENSE_KEY="${ENVGUARD_LICENSE_KEY:-}"

declare -A TIER_LEVELS=(
  [free]=0
  [pro]=1
  [team]=2
  [enterprise]=3
)

get_envguard_key() {
  if [[ -n "$ENVGUARD_LICENSE_KEY" ]]; then
    echo "$ENVGUARD_LICENSE_KEY"
    return 0
  fi

  if [[ -f "$OPENCLAW_CONFIG" ]]; then
    local key=""
    if command -v python3 &>/dev/null; then
      key=$(python3 -c "
import json
try:
    with open('$OPENCLAW_CONFIG') as f:
        cfg = json.load(f)
    print(cfg.get('skills', {}).get('entries', {}).get('envguard', {}).get('apiKey', ''))
except: pass
" 2>/dev/null)
    elif command -v node &>/dev/null; then
      key=$(node -e "
try {
  const cfg = require('$OPENCLAW_CONFIG');
  console.log(cfg?.skills?.entries?.envguard?.apiKey || '');
} catch(e) {}
" 2>/dev/null)
    elif command -v jq &>/dev/null; then
      key=$(jq -r '.skills.entries.envguard.apiKey // ""' "$OPENCLAW_CONFIG" 2>/dev/null)
    fi

    if [[ -n "${key:-}" ]]; then
      echo "$key"
      return 0
    fi
  fi
  return 1
}

decode_jwt_payload() {
  local token="$1"

  # --- JWT Structure Validation ---
  # A valid JWT must have exactly 3 dot-separated parts: header.payload.signature
  local dot_count
  dot_count=$(echo "$token" | tr -cd '.' | wc -c)
  if [[ "$dot_count" -ne 2 ]]; then
    echo "[JWT] Malformed [REDACTED_SECRET] 3 parts (header.payload.signature), got $((dot_count + 1))" >&2
    return 1
  fi

  local header payload signature
  header=$(echo "$token" | cut -d. -f1)
  payload=$(echo "$token" | cut -d. -f2)
  signature=$(echo "$token" | cut -d. -f3)

  # Signature must be non-empty
  if [[ -z "$signature" ]]; then
    echo "[JWT] Rejected: signature segment is empty (unsigned token)" >&2
    return 1
  fi

  # --- Signature Verification (HMAC-SHA256 via openssl, if available) ---
  # NOTE: Full cryptographic verification requires the shared secret. When
  # CLAWHUB_JWT_SECRET is set, we verify the HMAC-SHA256 signature. Without
  # the secret, we still enforce structural integrity (3-part, non-empty sig)
  # which blocks trivially forged tokens (e.g., alg:none attacks).
  if [[ -n "${CLAWHUB_JWT_SECRET:-}" ]] && command -v openssl &>/dev/null; then
    local signing_input="${header}.${payload}"
    local expected_sig
    expected_sig=$(printf '%s' "$signing_input" \
      | openssl dgst -sha256 -hmac "$CLAWHUB_JWT_SECRET" -binary 2>/dev/null \
      | openssl base64 -e 2>/dev/null \
      | tr '/+' '_-' | tr -d '=')
    if [[ "$expected_sig" != "$signature" ]]; then
      echo "[JWT] Signature verification failed" >&2
      return 1
    fi
  fi

  # --- Decode Payload ---
  local padded="$payload"
  local mod=$((${#payload} % 4))
  if [[ $mod -eq 2 ]]; then padded="${payload}=="; fi
  if [[ $mod -eq 3 ]]; then padded="${payload}="; fi

  local decoded
  decoded=$(echo "$padded" | tr '_-' '/+' | base64 -d 2>/dev/null || \
            echo "$padded" | tr '_-' '/+' | base64 -D 2>/dev/null || \
            echo "$padded" | tr '_-' '/+' | openssl base64 -d 2>/dev/null) || {
    echo "[JWT] Failed to decode payload" >&2
    return 1
  }

  # --- Expiry Check ---
  # Check exp claim inline to catch expired tokens at decode time
  local exp_val=""
  if command -v python3 &>/dev/null; then
    exp_val=$(python3 -c "import json; print(json.loads('$decoded').get('exp', ''))" 2>/dev/null || true)
  elif command -v node &>/dev/null; then
    exp_val=$(node -e "console.log(JSON.parse('$decoded').exp || '')" 2>/dev/null || true)
  elif command -v jq &>/dev/null; then
    exp_val=$(echo "$decoded" | jq -r '.exp // ""' 2>/dev/null || true)
  fi

  if [[ -n "$exp_val" && "$exp_val" != "null" ]]; then
    local now
    now=$(date +%s)
    if [[ "$now" -gt "$exp_val" ]]; then
      echo "[JWT] Token expired (exp=$exp_val, now=$now)" >&2
      return 1
    fi
  fi

  echo "$decoded"
}

extract_field() {
  local json="$1" field="$2"
  if command -v python3 &>/dev/null; then
    python3 -c "import json; print(json.loads('$json').get('$field', ''))" 2>/dev/null
  elif command -v node &>/dev/null; then
    node -e "console.log(JSON.parse('$json')['$field'] || '')" 2>/dev/null
  elif command -v jq &>/dev/null; then
    echo "$json" | jq -r ".$field // \"\"" 2>/dev/null
  else
    echo "$json" | grep -oP "\"$field\"\s*:\s*\"?\K[^\",$}]+" 2>/dev/null || echo ""
  fi
}

check_envguard_license() {
  local required_tier="${1:-pro}"
  local required_level="${TIER_LEVELS[$required_tier]:-1}"

  local key
  if ! key=$(get_envguard_key) || [[ -z "$key" ]]; then
    echo -e "${RED}${BOLD}━━━ EnvGuard License Required ━━━${NC}"
    echo ""
    echo -e "This feature requires a ${BOLD}${required_tier}${NC} license."
    echo ""
    echo -e "Get your license at: ${CYAN}${BOLD}https://envguard.pages.dev/pricing${NC}"
    echo ""
    echo "Then configure it:"
    echo -e "  ${DIM}# In ~/.openclaw/openclaw.json:${NC}"
    echo -e "  ${CYAN}\"skills\": { \"entries\": { \"envguard\": { \"apiKey\": \"YOUR_KEY\" } } }${NC}"
    echo ""
    echo -e "  ${DIM}# Or set the environment variable:${NC}"
    echo -e "  ${CYAN}export ENVGUARD_LICENSE_KEY=\"YOUR_KEY\"${NC}"
    return 1
  fi

  local payload
  if ! payload=$(decode_jwt_payload "$key"); then
    echo -e "${RED}[EnvGuard]${NC} Invalid license key."
    echo -e "Get a valid license at: ${CYAN}https://envguard.pages.dev/pricing${NC}"
    return 1
  fi

  local tier expiry
  tier=$(extract_field "$payload" "tier")
  expiry=$(extract_field "$payload" "exp")

  local actual_level="${TIER_LEVELS[$tier]:-0}"
  if [[ "$actual_level" -lt "$required_level" ]]; then
    echo -e "${RED}[EnvGuard]${NC} This feature requires ${BOLD}$required_tier${NC} tier. You have: ${BOLD}$tier${NC}"
    echo -e "Upgrade at: ${CYAN}https://envguard.pages.dev/upgrade${NC}"
    return 1
  fi

  if [[ -n "$expiry" ]]; then
    local now
    now=$(date +%s)
    if [[ "$now" -gt "$expiry" ]]; then
      echo -e "${RED}[EnvGuard]${NC} License expired. Renew at: ${CYAN}https://envguard.pages.dev/renew${NC}"
      return 1
    fi
  fi

  return 0
}

show_envguard_status() {
  local key
  if ! key=$(get_envguard_key) || [[ -z "$key" ]]; then
    echo -e "License: ${YELLOW}Free tier${NC}"
    echo -e "  Available: scan"
    echo -e "  Upgrade at: ${CYAN}https://envguard.pages.dev/pricing${NC}"
    return
  fi

  local payload tier expiry seats
  payload=$(decode_jwt_payload "$key" 2>/dev/null) || { echo -e "License: ${RED}Invalid key${NC}"; return; }
  tier=$(extract_field "$payload" "tier")
  expiry=$(extract_field "$payload" "exp")
  seats=$(extract_field "$payload" "seats")

  local now status_color status_text
  now=$(date +%s)
  status_color="$GREEN"; status_text="Active"
  if [[ -n "$expiry" && "$now" -gt "$expiry" ]]; then
    status_color="$RED"; status_text="Expired"
  fi

  local expiry_date="Never"
  [[ -n "$expiry" ]] && expiry_date=$(date -d "@$expiry" +%Y-%m-%d 2>/dev/null || date -r "$expiry" +%Y-%m-%d 2>/dev/null || echo "unknown")

  echo -e "License: ${status_color}${BOLD}${tier^^}${NC} (${status_color}${status_text}${NC})"
  echo -e "  Seats: ${seats:-unlimited}"
  echo -e "  Expires: $expiry_date"
  echo ""

  case "$tier" in
    pro)
      echo -e "  Available: scan, hooks, allowlist, diff"
      echo -e "  Upgrade to Team for: history, report, policy"
      ;;
    team)
      echo -e "  Available: scan, hooks, allowlist, diff, history, report, policy"
      ;;
    enterprise)
      echo -e "  Available: All features + priority support"
      ;;
  esac
}


path: scripts/patterns.sh
content: #!/usr/bin/env bash
# EnvGuard — Secret Pattern Definitions
# Each pattern: REGEX | SEVERITY | SERVICE | DESCRIPTION
#
# Severity levels:
#   critical — Immediate credential exposure risk
#   high     — Likely valid secret or token
#   medium   — Possible secret, needs verification
#   low      — Generic pattern, higher false positive rate
#
# IMPORTANT: All regexes must use POSIX ERE syntax (grep -E compatible).
# - Use [[:space:]] instead of \s
# - Use literal quotes instead of \x27
# - Avoid Perl-only features (\d, \w, etc.)

set -euo pipefail

# ─── Pattern registry ──────────────────────────────────────────────────────
#
# Format: "regex|severity|service|description"
# Patterns use POSIX extended grep regex (ERE) syntax.
# Order: critical first, then high, medium, low.

declare -a ENVGUARD_PATTERNS=()

# ─── 1. AWS Credentials (Critical) ────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'AKIA[0-9A-Z]{16}|critical|AWS|AWS Access Key ID'
  'aws_secret_access_key[[:space:]]*[=:][[:space:]]*[A-Za-z0-9/+=]{40}|critical|AWS|AWS Secret Access Key'
  'ASIA[0-9A-Z]{16}|critical|AWS|AWS Temporary Access Key (STS)'
)

# ─── 2. Stripe Keys (Critical) ────────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'sk_live_[0-9a-zA-Z]{24,}|critical|Stripe|Stripe Live Secret Key'
  'sk_test_[0-9a-zA-Z]{24,}|high|Stripe|Stripe Test Secret Key'
  'rk_live_[0-9a-zA-Z]{24,}|critical|Stripe|Stripe Live Restricted Key'
  'rk_test_[0-9a-zA-Z]{24,}|high|Stripe|Stripe Test Restricted Key'
  'whsec_[0-9a-zA-Z]{24,}|critical|Stripe|Stripe Webhook Signing Secret'
  'pk_live_[0-9a-zA-Z]{24,}|medium|Stripe|Stripe Live Publishable Key'
)

# ─── 3. GitHub Tokens (Critical) ──────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'ghp_[0-9a-zA-Z]{36,}|critical|GitHub|GitHub Personal Access Token'
  'gho_[0-9a-zA-Z]{36,}|critical|GitHub|GitHub OAuth Access Token'
  'ghu_[0-9a-zA-Z]{36,}|critical|GitHub|GitHub User-to-Server Token'
  'ghs_[0-9a-zA-Z]{36,}|critical|GitHub|GitHub Server-to-Server Token'
  'ghr_[0-9a-zA-Z]{36,}|critical|GitHub|GitHub Refresh Token'
  'github_pat_[0-9a-zA-Z_]{22,}|critical|GitHub|GitHub Fine-Grained Personal Access Token'
)

# ─── 4. GitLab Tokens (Critical) ──────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'glpat-[0-9a-zA-Z_-]{20,}|critical|GitLab|GitLab Personal Access Token'
  'gloas-[0-9a-zA-Z_-]{20,}|critical|GitLab|GitLab OAuth Application Secret'
  'glrt-[0-9a-zA-Z_-]{20,}|critical|GitLab|GitLab Runner Registration Token'
)

# ─── 5. Slack Tokens (High) ───────────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'xoxb-[0-9]{10,}-[0-9a-zA-Z]{24,}|high|Slack|Slack Bot Token'
  'xoxp-[0-9]{10,}-[0-9a-zA-Z]{24,}|high|Slack|Slack User OAuth Token'
  'xoxo-[0-9]{10,}-[0-9a-zA-Z]{24,}|high|Slack|Slack OAuth Token'
  'xapp-[0-9]+-[A-Za-z0-9]{10,}-[0-9]{10,}-[a-z0-9]{64}|high|Slack|Slack App-Level Token'
  'xoxs-[0-9a-zA-Z-]{50,}|high|Slack|Slack Session Token'
)

# ─── 6. Google API Keys (High) ────────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'AIza[0-9A-Za-z_-]{35}|high|Google|Google API Key'
  '[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com|high|Google|Google OAuth Client ID'
)

# ─── 7. JWT Tokens (High) ─────────────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}|high|JWT|JSON Web Token'
)

# ─── 8. Generic API Keys and Secrets (Low) ────────────────────────────────

ENVGUARD_PATTERNS+=(
  '[Aa][Pp][Ii]_?[Kk][Ee][Yy][[:space:]]*[=:][[:space:]]*"[0-9a-zA-Z_-]{16,}"|low|Generic|Generic API Key assignment'
  "[Aa][Pp][Ii]_?[Kk][Ee][Yy][[:space:]]*[=:][[:space:]]*'[0-9a-zA-Z_-]{16,}'|low|Generic|Generic API Key assignment (single-quoted)"
  '[Aa][Pp][Ii][-_]?[Kk][Ee][Yy][[:space:]]*[=:][[:space:]]*[0-9a-zA-Z_-]{20,}|low|Generic|Generic API Key assignment (unquoted)'
  '[Ss][Ee][Cc][Rr][Ee][Tt][[:space:]]*[=:][[:space:]]*"[^ "]{8,}"|low|Generic|Generic secret assignment'
  '[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd][[:space:]]*[=:][[:space:]]*"[^ "]{8,}"|low|Generic|Generic password assignment'
  '[Tt][Oo][Kk][Ee][Nn][[:space:]]*[=:][[:space:]]*"[0-9a-zA-Z_-]{16,}"|low|Generic|Generic token assignment'
  'auth[_-]?token[[:space:]]*[=:][[:space:]]*"[^ "]{8,}"|low|Generic|Auth token assignment'
  'access[_-]?token[[:space:]]*[=:][[:space:]]*"[^ "]{8,}"|low|Generic|Access token assignment'
)

# ─── 9. Private Keys (Critical) ───────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'BEGIN RSA PRIVATE KEY|critical|PrivateKey|RSA Private Key'
  'BEGIN OPENSSH PRIVATE KEY|critical|PrivateKey|OpenSSH Private Key'
  'BEGIN DSA PRIVATE KEY|critical|PrivateKey|DSA Private Key'
  'BEGIN EC PRIVATE KEY|critical|PrivateKey|EC Private Key'
  'BEGIN PGP PRIVATE KEY BLOCK|critical|PrivateKey|PGP Private Key Block'
  'BEGIN PRIVATE KEY|critical|PrivateKey|Generic Private Key (PKCS8)'
  'BEGIN ENCRYPTED PRIVATE KEY|high|PrivateKey|Encrypted Private Key'
)

# ─── 10. .env File Contents (Low) ─────────────────────────────────────────
# Detects KEY=value patterns that look like env vars in non-.env files

ENVGUARD_PATTERNS+=(
  '^[A-Z][A-Z0-9_]{2,}=[[:space:]]*"?[^ "]{8,}"?|low|DotEnv|Environment variable pattern (potential .env leak)'
)

# ─── 11. Database Connection Strings (High) ───────────────────────────────

ENVGUARD_PATTERNS+=(
  'postgres(ql)?://[^ "<>]{10,}|high|Database|PostgreSQL Connection String'
  'mysql://[^ "<>]{10,}|high|Database|MySQL Connection String'
  'mongodb(\+srv)?://[^ "<>]{10,}|high|Database|MongoDB Connection String'
  'redis://[^ "<>]{10,}|high|Database|Redis Connection String'
  'amqp://[^ "<>]{10,}|high|Database|AMQP/RabbitMQ Connection String'
  'mssql://[^ "<>]{10,}|high|Database|MSSQL Connection String'
)

# ─── 12. SSH Keys / PEM Files (Medium) ────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'BEGIN CERTIFICATE|medium|Certificate|X.509 Certificate'
  'ssh-rsa[[:space:]]+AAAA[0-9A-Za-z+/]{50,}|medium|SSH|SSH RSA Public Key (check for private key nearby)'
  'ssh-ed25519[[:space:]]+AAAA[0-9A-Za-z+/]{50,}|medium|SSH|SSH Ed25519 Public Key'
)

# ─── 13. Twilio, SendGrid, Mailgun, Postmark (High) ──────────────────────

ENVGUARD_PATTERNS+=(
  'SK[0-9a-fA-F]{32}|high|Twilio|Twilio API Key'
  'AC[0-9a-fA-F]{32}|medium|Twilio|Twilio Account SID'
  'SG\.[0-9A-Za-z_-]{22}\.[0-9A-Za-z_-]{43}|high|SendGrid|SendGrid API Key'
  'key-[0-9a-zA-Z]{32}|medium|Mailgun|Mailgun API Key'
  '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|low|Postmark|Postmark Server Token (UUID pattern)'
)

# ─── 14. Firebase / Supabase / Vercel (High) ──────────────────────────────

ENVGUARD_PATTERNS+=(
  'firebase[_-]?api[_-]?key[[:space:]]*[=:][[:space:]]*"?AIza[0-9A-Za-z_-]{35}|high|Firebase|Firebase API Key'
  'FIREBASE_[A-Z_]*[[:space:]]*[=:][[:space:]]*"[^ "]{10,}"|medium|Firebase|Firebase Configuration Value'
  'sbp_[0-9a-f]{40}|high|Supabase|Supabase Service Role Key'
  'vercel_[0-9a-zA-Z_]{24,}|high|Vercel|Vercel Token'
  'VERCEL_[A-Z_]*[[:space:]]*[=:][[:space:]]*"[^ "]{10,}"|medium|Vercel|Vercel Environment Variable'
)

# ─── 15. npm Tokens (High) ────────────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'npm_[0-9a-zA-Z]{36,}|high|npm|npm Access Token'
  '//registry\.npmjs\.org/:_authToken=[0-9a-zA-Z_-]{36,}|high|npm|npm Registry Auth Token'
)

# ─── 16. Docker Hub Tokens (Medium) ───────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'dckr_pat_[0-9a-zA-Z_-]{24,}|medium|Docker|Docker Hub Personal Access Token'
  'DOCKER_[A-Z_]*PASSWORD[[:space:]]*[=:][[:space:]]*"[^ "]{8,}"|medium|Docker|Docker Password in Config'
)

# ─── 17. Heroku API Keys (Medium) ─────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'heroku[_-]?api[_-]?key[[:space:]]*[=:][[:space:]]*"?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|medium|Heroku|Heroku API Key'
  'HEROKU_API_KEY[[:space:]]*[=:][[:space:]]*"?[0-9a-fA-F-]{36}|medium|Heroku|Heroku API Key (env var)'
)

# ─── 18. DigitalOcean Tokens (Medium) ─────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'dop_v1_[0-9a-fA-F]{64}|medium|DigitalOcean|DigitalOcean Personal Access Token'
  'doo_v1_[0-9a-fA-F]{64}|medium|DigitalOcean|DigitalOcean OAuth Token'
)

# ─── 19. Azure Keys (Medium) ──────────────────────────────────────────────

ENVGUARD_PATTERNS+=(
  'AccountKey=[0-9A-Za-z+/=]{44,}|medium|Azure|Azure Storage Account Key'
  'azure[_-]?client[_-]?secret[[:space:]]*[=:][[:space:]]*"[^ "]{8,}"|medium|Azure|Azure Client Secret'
  'AZURE_[A-Z_]*KEY[[:space:]]*[=:][[:space:]]*"[^ "]{10,}"|medium|Azure|Azure API Key'
  'SharedAccessSignature=sv=[^ &]{20,}|medium|Azure|Azure Shared Access Signature'
)

# ─── 20. Cloudflare API Tokens (Medium) ───────────────────────────────────

ENVGUARD_PATTERNS+=(
  'cloudflare[_-]?api[_-]?token[[:space:]]*[=:][[:space:]]*"?[0-9a-zA-Z_-]{40}|medium|Cloudflare|Cloudflare API Token'
  'CF_API_TOKEN[[:space:]]*[=:][[:space:]]*"?[0-9a-zA-Z_-]{40}|medium|Cloudflare|Cloudflare API Token (env var)'
  'CF_API_KEY[[:space:]]*[=:][[:space:]]*"?[0-9a-fA-F]{37}|medium|Cloudflare|Cloudflare Global API Key'
)

# ─── Utility: get pattern count ────────────────────────────────────────────

envguard_pattern_count() {
  echo "${#ENVGUARD_PATTERNS[@]}"
}

# ─── Utility: list patterns by severity ────────────────────────────────────

envguard_list_patterns() {
  local filter_severity="${1:-all}"

  for entry in "${ENVGUARD_PATTERNS[@]}"; do
    IFS='|' read -r regex severity service description <<< "$entry"
    if [[ "$filter_severity" == "all" || "$filter_severity" == "$severity" ]]; then
      printf "%-10s %-15s %s\n" "$severity" "$service" "$description"
    fi
  done
}

# ─── Utility: severity to numeric level ────────────────────────────────────

severity_to_level() {
  case "$1" in
    critical) echo 4 ;;
    high)     echo 3 ;;
    medium)   echo 2 ;;
    low)      echo 1 ;;
    *)        echo 0 ;;
  esac
}

# ─── Default exclude directories ───────────────────────────────────────────

ENVGUARD_EXCLUDE_DIRS=(
  ".git"
  "node_modules"
  "dist"
  "build"
  "vendor"
  "__pycache__"
  ".venv"
  "venv"
  ".tox"
  ".mypy_cache"
  ".pytest_cache"
  "target"
  ".next"
  ".nuxt"
  "coverage"
  ".terraform"
)

# ─── Default binary extensions to skip ─────────────────────────────────────

ENVGUARD_BINARY_EXTENSIONS=(
  "png" "jpg" "jpeg" "gif" "bmp" "ico" "svg"
  "woff" "woff2" "ttf" "eot" "otf"
  "pdf" "zip" "tar" "gz" "bz2" "xz" "7z" "rar"
  "exe" "dll" "so" "dylib" "bin"
  "mp3" "mp4" "avi" "mov" "mkv" "wav" "flac"
  "pyc" "pyo" "class" "o" "obj"
  "wasm" "map"
)


path: scripts/scan.sh
content: #!/usr/bin/env bash
# scan.sh — Minimal wrapper that sources patterns and runs the scan
# Generated by generate-skills.sh — do not edit manually
#
# Usage: bash scan.sh [file|directory]
#   Defaults to scanning current directory if no argument given.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Source patterns for envguard
source "$SCRIPT_DIR/patterns.sh"

# Delegate to main entry point
exec bash "$SCRIPT_DIR/envguard.sh" scan "${@:-.}"


path: scripts/scanner.sh
content: #!/usr/bin/env bash
# EnvGuard — Secret Scanning Engine
# Scans files and directories for leaked secrets, credentials, and API keys

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# Load patterns
source "$SCRIPT_DIR/patterns.sh"

OPENCLAW_CONFIG="${HOME}/.openclaw/openclaw.json"

# ─── Redact sensitive values ──────────────────────────────────────────────
# Show first 4 and last 4 chars, mask the rest

redact_match() {
  local match="$1"
  local len=${#match}

  if [[ $len -le 12 ]]; then
    # Too short to meaningfully redact — show first 4 + stars
    echo "${match:0:4}********"
  else
    local prefix="${match:0:4}"
    local suffix="${match: -4}"
    local mask_len=$((len - 8))
    local mask=""
    for ((i = 0; i < mask_len && i < 20; i++)); do
      mask+="*"
    done
    echo "${prefix}${mask}${suffix}"
  fi
}

# ─── Severity colorization ────────────────────────────────────────────────

severity_color() {
  case "$1" in
    critical) echo -e "${RED}${BOLD}CRITICAL${NC}" ;;
    high)     echo -e "${RED}HIGH${NC}" ;;
    medium)   echo -e "${YELLOW}MEDIUM${NC}" ;;
    low)      echo -e "${BLUE}LOW${NC}" ;;
    *)        echo -e "${DIM}UNKNOWN${NC}" ;;
  esac
}

severity_icon() {
  case "$1" in
    critical) echo -e "${RED}!!${NC}" ;;
    high)     echo -e "${RED}!${NC}" ;;
    medium)   echo -e "${YELLOW}~${NC}" ;;
    low)      echo -e "${BLUE}-${NC}" ;;
    *)        echo "?" ;;
  esac
}

# ─── Load allowlist from config ───────────────────────────────────────────

load_allowlist() {
  local allowlist=()

  if [[ -f "$OPENCLAW_CONFIG" ]]; then
    local raw=""
    if command -v python3 &>/dev/null; then
      raw=$(python3 -c "
import json
try:
    with open('$OPENCLAW_CONFIG') as f:
        cfg = json.load(f)
    items = cfg.get('skills', {}).get('entries', {}).get('envguard', {}).get('config', {}).get('allowlist', [])
    for item in items:
        print(item)
except: pass
" 2>/dev/null) || true
    elif command -v node &>/dev/null; then
      raw=$(node -e "
try {
  const cfg = require('$OPENCLAW_CONFIG');
  const items = cfg?.skills?.entries?.envguard?.config?.allowlist || [];
  items.forEach(i => console.log(i));
} catch(e) {}
" 2>/dev/null) || true
    elif command -v jq &>/dev/null; then
      raw=$(jq -r '.skills.entries.envguard.config.allowlist[]? // empty' "$OPENCLAW_CONFIG" 2>/dev/null) || true
    fi

    while IFS= read -r line; do
      [[ -n "$line" ]] && allowlist+=("$line")
    done <<< "$raw"
  fi

  printf '%s\n' "${allowlist[@]}" 2>/dev/null || true
}

# ─── Load .envguardignore patterns ────────────────────────────────────────

load_ignore_patterns() {
  local dir="$1"
  local ignore_file="$dir/.envguardignore"
  local patterns=()

  if [[ -f "$ignore_file" ]]; then
    while IFS= read -r line; do
      # Skip comments and empty lines
      [[ -z "$line" || "$line" =~ ^# ]] && continue
      patterns+=("$line")
    done < "$ignore_file"
  fi

  printf '%s\n' "${patterns[@]}" 2>/dev/null || true
}

# ─── Check if file should be ignored ──────────────────────────────────────

should_ignore_file() {
  local filepath="$1"
  local ignore_patterns="$2"

  # Check against default exclude directories
  for exclude_dir in "${ENVGUARD_EXCLUDE_DIRS[@]}"; do
    if [[ "$filepath" == *"/$exclude_dir/"* || "$filepath" == "$exclude_dir/"* ]]; then
      return 0
    fi
  done

  # Check against binary extensions
  local ext="${filepath##*.}"
  ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
  for bin_ext in "${ENVGUARD_BINARY_EXTENSIONS[@]}"; do
    if [[ "$ext" == "$bin_ext" ]]; then
      return 0
    fi
  done

  # Check against .envguardignore patterns
  if [[ -n "$ignore_patterns" ]]; then
    while IFS= read -r pattern; do
      [[ -z "$pattern" ]] && continue
      # Simple glob matching
      if [[ "$filepath" == $pattern ]]; then
        return 0
      fi
      # Check basename match
      local basename
      basename=$(basename "$filepath")
      if [[ "$basename" == $pattern ]]; then
        return 0
      fi
    done <<< "$ignore_patterns"
  fi

  return 1
}

# ─── Check if match is allowlisted ────────────────────────────────────────

is_allowlisted() {
  local match="$1"
  local allowlist="$2"

  [[ -z "$allowlist" ]] && return 1

  while IFS= read -r pattern; do
    [[ -z "$pattern" ]] && continue
    if [[ "$match" == *"$pattern"* ]]; then
      return 0
    fi
  done <<< "$allowlist"

  return 1
}

# ─── Check if file is binary ──────────────────────────────────────────────

is_binary_file() {
  local filepath="$1"

  # Check extension first (fast path)
  local ext="${filepath##*.}"
  ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
  for bin_ext in "${ENVGUARD_BINARY_EXTENSIONS[@]}"; do
    if [[ "$ext" == "$bin_ext" ]]; then
      return 0
    fi
  done

  # Check with file command if available
  if command -v file &>/dev/null; then
    local file_type
    file_type=$(file -b --mime-type "$filepath" 2>/dev/null) || return 1
    if [[ "$file_type" != text/* && "$file_type" != application/json && "$file_type" != application/xml && "$file_type" != application/javascript ]]; then
      return 0
    fi
  fi

  return 1
}

# ─── Scan a single file ───────────────────────────────────────────────────
# Returns findings as tab-separated lines:
# filepath \t line_number \t severity \t service \t description \t redacted_match

scan_file() {
  local filepath="$1"
  local allowlist="$2"
  local findings=0
  local is_env_file=false

  # Check if this is actually an .env file (skip .env leak patterns for .env files)
  local basename
  basename=$(basename "$filepath")
  if [[ "$basename" == .env* || "$basename" == *.env || "$basename" == ".env" ]]; then
    is_env_file=true
  fi

  # Read file line by line
  local line_num=0
  while IFS= read -r line || [[ -n "$line" ]]; do
    ((line_num++)) || true

    # Skip empty lines and comment-only lines for performance
    [[ -z "$line" ]] && continue

    # Test each pattern
    for entry in "${ENVGUARD_PATTERNS[@]}"; do
      IFS='|' read -r regex severity service description <<< "$entry"

      # Skip .env patterns for actual .env files
      if [[ "$is_env_file" == "true" && "$service" == "DotEnv" ]]; then
        continue
      fi

      # Match the line against the pattern
      local match=""
      match=$(echo "$line" | grep -oE -- "$regex" 2>/dev/null | head -1) || true

      if [[ -n "$match" ]]; then
        # Check allowlist
        if is_allowlisted "$match" "$allowlist"; then
          continue
        fi

        local redacted
        redacted=$(redact_match "$match")

        # Output finding
        printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
          "$filepath" "$line_num" "$severity" "$service" "$description" "$redacted"

        ((findings++)) || true

        # Only report first match per line to avoid duplicates
        break
      fi
    done
  done < "$filepath"

  return 0
}

# ─── Find all scannable files ─────────────────────────────────────────────

find_scannable_files() {
  local target="$1"
  local ignore_patterns="$2"

  if [[ -f "$target" ]]; then
    # Single file
    if ! is_binary_file "$target"; then
      echo "$target"
    fi
    return
  fi

  # Build find exclude arguments
  local exclude_args=""
  for dir in "${ENVGUARD_EXCLUDE_DIRS[@]}"; do
    exclude_args="$exclude_args -name $dir -prune -o"
  done

  # Find all regular files, excluding directories
  if command -v find &>/dev/null; then
    find "$target" \
      -name ".git" -prune -o \
      -name "node_modules" -prune -o \
      -name "dist" -prune -o \
      -name "build" -prune -o \
      -name "vendor" -prune -o \
      -name "__pycache__" -prune -o \
      -name ".venv" -prune -o \
      -name "venv" -prune -o \
      -name "target" -prune -o \
      -name ".next" -prune -o \
      -name ".nuxt" -prune -o \
      -name "coverage" -prune -o \
      -name ".terraform" -prune -o \
      -type f -print 2>/dev/null | while IFS= read -r file; do
        # Skip binary files and ignored files
        if ! is_binary_file "$file" && ! should_ignore_file "$file" "$ignore_patterns"; then
          echo "$file"
        fi
      done
  fi
}

# ─── Main scan function ──────────────────────────────────────────────────

do_secret_scan() {
  local target="${1:-.}"
  local quiet="${2:-false}"

  # Resolve absolute path
  if [[ ! -e "$target" ]]; then
    echo -e "${RED}[EnvGuard]${NC} Target not found: ${BOLD}$target${NC}" >&2
    return 1
  fi

  # Load configurations
  local allowlist ignore_patterns
  allowlist=$(load_allowlist)

  local scan_root="$target"
  [[ -f "$target" ]] && scan_root=$(dirname "$target")
  ignore_patterns=$(load_ignore_patterns "$scan_root")

  # Collect scannable files
  local files_tmp
  files_tmp=$(mktemp)
  find_scannable_files "$target" "$ignore_patterns" > "$files_tmp"

  local file_count
  file_count=$(wc -l < "$files_tmp" | tr -d ' ')

  if [[ "$file_count" -eq 0 ]]; then
    if [[ "$quiet" != "true" ]]; then
      echo -e "${GREEN}[EnvGuard]${NC} No scannable files found in ${BOLD}$target${NC}"
    fi
    rm -f "$files_tmp"
    return 0
  fi

  if [[ "$quiet" != "true" ]]; then
    echo -e "${BOLD}━━━ EnvGuard Secret Scan ━━━${NC}"
    echo ""
    echo -e "Target:   ${BOLD}$target${NC}"
    echo -e "Files:    ${CYAN}$file_count${NC}"
    echo -e "Patterns: ${CYAN}$(envguard_pattern_count)${NC}"
    echo ""
  fi

  # Scan all files
  local findings_tmp
  findings_tmp=$(mktemp)
  local scanned=0

  while IFS= read -r file; do
    [[ -z "$file" ]] && continue
    ((scanned++)) || true

    # Progress indicator for large scans
    if [[ "$quiet" != "true" && $((scanned % 50)) -eq 0 ]]; then
      echo -ne "\r${DIM}  Scanning... $scanned / $file_count files${NC}"
    fi

    scan_file "$file" "$allowlist" >> "$findings_tmp" 2>/dev/null || true
  done < "$files_tmp"

  # Clear progress line
  if [[ "$quiet" != "true" && "$scanned" -ge 50 ]]; then
    echo -ne "\r\033[K"
  fi

  # Count findings by severity
  local total=0 critical=0 high=0 medium=0 low=0

  while IFS=$'\t' read -r f_file f_line f_severity f_service f_desc f_redacted; do
    [[ -z "$f_file" ]] && continue
    ((total++)) || true
    case "$f_severity" in
      critical) ((critical++)) || true ;;
      high)     ((high++)) || true ;;
      medium)   ((medium++)) || true ;;
      low)      ((low++)) || true ;;
    esac
  done < "$findings_tmp"

  # Display findings
  if [[ "$total" -gt 0 ]]; then
    if [[ "$quiet" != "true" ]]; then
      echo -e "${RED}${BOLD}Secrets detected!${NC}"
      echo ""

      # Sort by severity: critical first
      local sorted_tmp
      sorted_tmp=$(mktemp)
      {
        grep "^[^\t]*\tcritical\t" "$findings_tmp" 2>/dev/null || true
        grep "^[^\t]*\t[0-9]*\tcritical\t" "$findings_tmp" 2>/dev/null || true
      } > "$sorted_tmp" 2>/dev/null || true

      # Display each finding
      while IFS=$'\t' read -r f_file f_line f_severity f_service f_desc f_redacted; do
        [[ -z "$f_file" ]] && continue
        local icon
        icon=$(severity_icon "$f_severity")
        local sev_display
        sev_display=$(severity_color "$f_severity")

        echo -e "  $icon ${BOLD}$f_file${NC}:${CYAN}$f_line${NC}"
        echo -e "    $sev_display  ${DIM}$f_service${NC} - $f_desc"
        echo -e "    Match: ${DIM}$f_redacted${NC}"
        echo ""
      done < "$findings_tmp"

      rm -f "$sorted_tmp"

      # Summary
      echo -e "${BOLD}━━━ Summary ━━━${NC}"
      echo -e "  Files scanned: ${CYAN}$scanned${NC}"
      echo -e "  Total findings: ${BOLD}$total${NC}"
      [[ "$critical" -gt 0 ]] && echo -e "  ${RED}${BOLD}Critical: $critical${NC}"
      [[ "$high" -gt 0 ]]     && echo -e "  ${RED}High:     $high${NC}"
      [[ "$medium" -gt 0 ]]   && echo -e "  ${YELLOW}Medium:   $medium${NC}"
      [[ "$low" -gt 0 ]]      && echo -e "  ${BLUE}Low:      $low${NC}"
      echo ""

      # Remediation advice
      echo -e "${BOLD}Remediation:${NC}"
      echo -e "  1. Remove secrets from source code"
      echo -e "  2. Rotate any exposed credentials immediately"
      echo -e "  3. Use environment variables or a secret manager"
      echo -e "  4. Add sensitive files to .gitignore"
      echo -e "  5. Add false positives to allowlist: ${CYAN}envguard allowlist add <pattern>${NC}"
    fi
  else
    if [[ "$quiet" != "true" ]]; then
      echo -e "${GREEN}${BOLD}No secrets detected.${NC}"
      echo ""
      echo -e "  Files scanned: ${CYAN}$scanned${NC}"
      echo -e "  Patterns checked: ${CYAN}$(envguard_pattern_count)${NC}"
    fi
  fi

  # Store findings path for report generation
  ENVGUARD_LAST_FINDINGS="$findings_tmp"
  ENVGUARD_LAST_STATS="total=$total critical=$critical high=$high medium=$medium low=$low scanned=$scanned"

  rm -f "$files_tmp"

  # Exit with error if critical or high findings
  if [[ "$critical" -gt 0 || "$high" -gt 0 ]]; then
    return 1
  fi

  return 0
}

# ─── Scan staged changes only (PRO) ──────────────────────────────────────

do_diff_scan() {
  local allowlist
  allowlist=$(load_allowlist)

  echo -e "${BOLD}━━━ EnvGuard Staged Changes Scan ━━━${NC}"
  echo ""

  # Get staged files
  local staged_files
  staged_files=$(git diff --cached --name-only 2>/dev/null) || {
    echo -e "${RED}[EnvGuard]${NC} Not inside a git repository or no staged files."
    return 1
  }

  if [[ -z "$staged_files" ]]; then
    echo -e "${GREEN}[EnvGuard]${NC} No staged files to scan."
    return 0
  fi

  local file_count
  file_count=$(echo "$staged_files" | wc -l | tr -d ' ')
  echo -e "Staged files: ${CYAN}$file_count${NC}"
  echo ""

  # Get diff content with line context
  local findings_tmp
  findings_tmp=$(mktemp)
  local total=0 critical=0 high=0 medium=0 low=0

  while IFS= read -r file; do
    [[ -z "$file" || ! -f "$file" ]] && continue

    # Skip binary files
    is_binary_file "$file" && continue

    # Get only added lines from diff
    local diff_output
    diff_output=$(git diff --cached -U0 "$file" 2>/dev/null) || continue

    local current_line=0
    while IFS= read -r diff_line; do
      # Track line numbers from @@ headers
      if [[ "$diff_line" =~ ^@@.*\+([0-9]+) ]]; then
        current_line="${BASH_REMATCH[1]}"
        continue
      fi

      # Only scan added lines
      if [[ "$diff_line" =~ ^\+ && ! "$diff_line" =~ ^\+\+\+ ]]; then
        local content="${diff_line:1}"

        for entry in "${ENVGUARD_PATTERNS[@]}"; do
          IFS='|' read -r regex severity service description <<< "$entry"

          local match=""
          match=$(echo "$content" | grep -oE -- "$regex" 2>/dev/null | head -1) || true

          if [[ -n "$match" ]]; then
            if is_allowlisted "$match" "$allowlist"; then
              continue
            fi

            local redacted
            redacted=$(redact_match "$match")

            printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
              "$file" "$current_line" "$severity" "$service" "$description" "$redacted" >> "$findings_tmp"

            ((total++)) || true
            case "$severity" in
              critical) ((critical++)) || true ;;
              high)     ((high++)) || true ;;
              medium)   ((medium++)) || true ;;
              low)      ((low++)) || true ;;
            esac

            break
          fi
        done

        ((current_line++)) || true
      fi
    done <<< "$diff_output"
  done <<< "$staged_files"

  # Display findings
  if [[ "$total" -gt 0 ]]; then
    echo -e "${RED}${BOLD}Secrets detected in staged changes!${NC}"
    echo ""

    while IFS=$'\t' read -r f_file f_line f_severity f_service f_desc f_redacted; do
      [[ -z "$f_file" ]] && continue
      local icon sev_display
      icon=$(severity_icon "$f_severity")
      sev_display=$(severity_color "$f_severity")

      echo -e "  $icon ${BOLD}$f_file${NC}:${CYAN}$f_line${NC}"
      echo -e "    $sev_display  ${DIM}$f_service${NC} - $f_desc"
      echo -e "    Match: ${DIM}$f_redacted${NC}"
      echo ""
    done < "$findings_tmp"

    echo -e "${BOLD}━━━ Summary ━━━${NC}"
    [[ "$critical" -gt 0 ]] && echo -e "  ${RED}${BOLD}Critical: $critical${NC}"
    [[ "$high" -gt 0 ]]     && echo -e "  ${RED}High:     $high${NC}"
    [[ "$medium" -gt 0 ]]   && echo -e "  ${YELLOW}Medium:   $medium${NC}"
    [[ "$low" -gt 0 ]]      && echo -e "  ${BLUE}Low:      $low${NC}"
  else
    echo -e "${GREEN}${BOLD}No secrets detected in staged changes.${NC}"
  fi

  rm -f "$findings_tmp"

  if [[ "$critical" -gt 0 || "$high" -gt 0 ]]; then
    return 1
  fi
  return 0
}

# ─── Git history scan (TEAM) ──────────────────────────────────────────────

do_history_scan() {
  local dir="${1:-.}"
  local allowlist
  allowlist=$(load_allowlist)

  echo -e "${BOLD}━━━ EnvGuard Git History Scan ━━━${NC}"
  echo ""
  echo -e "Target: ${BOLD}$dir${NC}"
  echo -e "${DIM}Scanning all commits for secrets... this may take a while.${NC}"
  echo ""

  local findings_tmp
  findings_tmp=$(mktemp)
  local total=0 critical=0 high=0 medium=0 low=0
  local commits_scanned=0

  # Walk git log
  (cd "$dir" && git log --all --diff-filter=A --diff-filter=M -p --format="%H|%an|%ai" 2>/dev/null) | {
    local current_commit="" current_author="" current_date="" current_file="" current_line=0

    while IFS= read -r line; do
      # Parse commit header
      if [[ "$line" =~ ^[0-9a-f]{40}\| ]]; then
        IFS='|' read -r current_commit current_author current_date <<< "$line"
        ((commits_scanned++)) || true

        # Progress
        if [[ $((commits_scanned % 100)) -eq 0 ]]; then
          echo -ne "\r${DIM}  Scanned $commits_scanned commits...${NC}"
        fi
        continue
      fi

      # Track file
      if [[ "$line" =~ ^diff\ --git\ a/.*\ b/(.*) ]]; then
        current_file="${BASH_REMATCH[1]}"
        current_line=0
        continue
      fi

      # Track line numbers
      if [[ "$line" =~ ^@@.*\+([0-9]+) ]]; then
        current_line="${BASH_REMATCH[1]}"
        continue
      fi

      # Only scan added lines
      if [[ "$line" =~ ^\+ && ! "$line" =~ ^\+\+\+ ]]; then
        local content="${line:1}"
        ((current_line++)) || true

        for entry in "${ENVGUARD_PATTERNS[@]}"; do
          IFS='|' read -r regex severity service description <<< "$entry"

          local match=""
          match=$(echo "$content" | grep -oE -- "$regex" 2>/dev/null | head -1) || true

          if [[ -n "$match" ]]; then
            if is_allowlisted "$match" "$allowlist"; then
              continue
            fi

            local redacted
            redacted=$(redact_match "$match")
            local short_commit="${current_commit:0:8}"

            printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
              "$current_file" "$current_line" "$severity" "$service" "$description" \
              "$redacted" "$short_commit" "$current_author" "$current_date" >> "$findings_tmp"

            ((total++)) || true
            case "$severity" in
              critical) ((critical++)) || true ;;
              high)     ((high++)) || true ;;
              medium)   ((medium++)) || true ;;
              low)      ((low++)) || true ;;
            esac

            break
          fi
        done
      fi
    done

    # Clear progress
    echo -ne "\r\033[K"

    # Display findings
    if [[ "$total" -gt 0 ]]; then
      echo -e "${RED}${BOLD}Secrets found in git history!${NC}"
      echo ""

      while IFS=$'\t' read -r f_file f_line f_severity f_service f_desc f_redacted f_commit f_author f_date; do
        [[ -z "$f_file" ]] && continue
        local icon sev_display
        icon=$(severity_icon "$f_severity")
        sev_display=$(severity_color "$f_severity")

        echo -e "  $icon ${BOLD}$f_file${NC}:${CYAN}$f_line${NC} ${DIM}(commit $f_commit by $f_author)${NC}"
        echo -e "    $sev_display  ${DIM}$f_service${NC} - $f_desc"
        echo -e "    Match: ${DIM}$f_redacted${NC}"
        echo ""
      done < "$findings_tmp"

      echo -e "${BOLD}━━━ History Scan Summary ━━━${NC}"
      echo -e "  Commits scanned: ${CYAN}$commits_scanned${NC}"
      echo -e "  Total findings:  ${BOLD}$total${NC}"
      [[ "$critical" -gt 0 ]] && echo -e "  ${RED}${BOLD}Critical: $critical${NC}"
      [[ "$high" -gt 0 ]]     && echo -e "  ${RED}High:     $high${NC}"
      [[ "$medium" -gt 0 ]]   && echo -e "  ${YELLOW}Medium:   $medium${NC}"
      [[ "$low" -gt 0 ]]      && echo -e "  ${BLUE}Low:      $low${NC}"
      echo ""
      echo -e "${BOLD}Remediation:${NC}"
      echo "  Secrets found in git history require credential rotation."
      echo "  Even deleted secrets remain in git objects."
      echo -e "  Consider: ${CYAN}git filter-branch${NC} or ${CYAN}BFG Repo-Cleaner${NC} to purge history."
    else
      echo -e "${GREEN}${BOLD}No secrets found in git history.${NC}"
      echo -e "  Commits scanned: ${CYAN}$commits_scanned${NC}"
    fi
  }

  rm -f "$findings_tmp"
}

# ─── Report generation (TEAM) ─────────────────────────────────────────────

do_secret_report() {
  local dir="${1:-.}"
  local output="$dir/ENVGUARD-REPORT.md"
  local project_name
  project_name=$(basename "$(cd "$dir" && pwd)")

  echo -e "${BLUE}[EnvGuard]${NC} Generating security report for ${BOLD}$dir${NC}"

  # Run scan quietly to collect findings
  local findings_tmp
  findings_tmp=$(mktemp)
  local total=0 critical=0 high=0 medium=0 low=0 scanned=0
  local allowlist ignore_patterns
  allowlist=$(load_allowlist)
  ignore_patterns=$(load_ignore_patterns "$dir")

  local files_tmp
  files_tmp=$(mktemp)
  find_scannable_files "$dir" "$ignore_patterns" > "$files_tmp"
  scanned=$(wc -l < "$files_tmp" | tr -d ' ')

  while IFS= read -r file; do
    [[ -z "$file" ]] && continue
    scan_file "$file" "$allowlist" >> "$findings_tmp" 2>/dev/null || true
  done < "$files_tmp"

  while IFS=$'\t' read -r f_file f_line f_severity f_service f_desc f_redacted; do
    [[ -z "$f_file" ]] && continue
    ((total++)) || true
    case "$f_severity" in
      critical) ((critical++)) || true ;;
      high)     ((high++)) || true ;;
      medium)   ((medium++)) || true ;;
      low)      ((low++)) || true ;;
    esac
  done < "$findings_tmp"

  # Generate report from template or inline
  local template="$SKILL_DIR/templates/report.md.tmpl"

  {
    echo "# EnvGuard Security Report: $project_name"
    echo ""
    echo "> Generated by [EnvGuard](https://envguard.pages.dev) on $(date +%Y-%m-%d' '%H:%M:%S)"
    echo ""
    echo "## Scan Summary"
    echo ""
    echo "| Metric | Value |"
    echo "|--------|-------|"
    echo "| Files scanned | $scanned |"
    echo "| Patterns checked | $(envguard_pattern_count) |"
    echo "| Total findings | $total |"
    echo "| Critical | $critical |"
    echo "| High | $high |"
    echo "| Medium | $medium |"
    echo "| Low | $low |"
    echo ""

    if [[ "$total" -gt 0 ]]; then
      echo "## Findings"
      echo ""
      echo "| File | Line | Severity | Service | Description | Match (redacted) |"
      echo "|------|------|----------|---------|-------------|------------------|"

      while IFS=$'\t' read -r f_file f_line f_severity f_service f_desc f_redacted; do
        [[ -z "$f_file" ]] && continue
        echo "| \`$f_file\` | $f_line | **$f_severity** | $f_service | $f_desc | \`$f_redacted\` |"
      done < "$findings_tmp"

      echo ""
      echo "## Severity Breakdown"
      echo ""
      [[ "$critical" -gt 0 ]] && echo "- **CRITICAL ($critical):** Immediate credential exposure. Rotate these credentials NOW."
      [[ "$high" -gt 0 ]]     && echo "- **HIGH ($high):** Likely valid secrets. Remove from source and rotate."
      [[ "$medium" -gt 0 ]]   && echo "- **MEDIUM ($medium):** Possible secrets. Verify and remediate if confirmed."
      [[ "$low" -gt 0 ]]      && echo "- **LOW ($low):** Generic patterns with higher false positive rate. Review manually."
      echo ""
      echo "## Remediation Steps"
      echo ""
      echo "1. **Remove** all secrets from source code immediately"
      echo "2. **Rotate** any exposed credentials (API keys, tokens, passwords)"
      echo "3. **Use** environment variables or a secrets manager (Vault, AWS SSM, etc.)"
      echo "4. **Add** sensitive files to \`.gitignore\`"
      echo "5. **Install** pre-commit hooks: \`envguard hooks install\`"
      echo "6. **Scan** git history: \`envguard history\` (may contain historical secrets)"
      echo "7. **Allowlist** false positives: \`envguard allowlist add <pattern>\`"
    else
      echo "## Result"
      echo ""
      echo "No secrets detected. Your codebase appears clean."
      echo ""
      echo "### Recommendations"
      echo ""
      echo "- Install pre-commit hooks to maintain this status: \`envguard hooks install\`"
      echo "- Periodically scan git history: \`envguard history\`"
      echo "- Add a CI/CD check: \`envguard scan --exit-code\`"
    fi

    echo ""
    echo "---"
    echo ""
    echo "*Report generated by [EnvGuard](https://envguard.pages.dev). Run \`envguard scan\` for interactive results.*"

  } > "$output"

  rm -f "$findings_tmp" "$files_tmp"

  echo -e "${GREEN}[EnvGuard]${NC} Report written to ${BOLD}$output${NC}"
}

# ─── Policy enforcement (TEAM) ────────────────────────────────────────────

do_policy_scan() {
  local dir="${1:-.}"

  echo -e "${BOLD}━━━ EnvGuard Policy Enforcement ━━━${NC}"
  echo ""

  # Load custom patterns from config
  local custom_patterns=()

  if [[ -f "$OPENCLAW_CONFIG" ]]; then
    if command -v python3 &>/dev/null; then
      local raw
      raw=$(python3 -c "
import json
try:
    with open('$OPENCLAW_CONFIG') as f:
        cfg = json.load(f)
    patterns = cfg.get('skills', {}).get('entries', {}).get('envguard', {}).get('config', {}).get('customPatterns', [])
    for p in patterns:
        print(p.get('regex', '') + '|' + p.get('severity', 'medium') + '|' + p.get('service', 'Custom') + '|' + p.get('description', 'Custom pattern'))
except: pass
" 2>/dev/null) || true

      while IFS= read -r pattern; do
        [[ -n "$pattern" ]] && custom_patterns+=("$pattern")
      done <<< "$raw"
    fi
  fi

  if [[ ${#custom_patterns[@]} -gt 0 ]]; then
    echo -e "Custom patterns loaded: ${CYAN}${#custom_patterns[@]}${NC}"

    # Add custom patterns to the pattern list temporarily
    for cp in "${custom_patterns[@]}"; do
      ENVGUARD_PATTERNS+=("$cp")
    done
  else
    echo -e "${DIM}No custom patterns configured.${NC}"
    echo -e "Add custom patterns in ${CYAN}~/.openclaw/openclaw.json${NC}:"
    echo -e "  ${DIM}envguard.config.customPatterns: [{ \"regex\": \"...\", \"severity\": \"high\", \"service\": \"MyApp\", \"description\": \"...\" }]${NC}"
    echo ""
  fi

  echo -e "Running scan with ${CYAN}$(envguard_pattern_count)${NC} patterns (built-in + custom)..."
  echo ""

  # Run the full scan
  do_secret_scan "$dir"
}

# ─── Hook entry point — called by lefthook ────────────────────────────────

hook_secret_scan() {
  local staged_files
  staged_files=$(git diff --cached --name-only 2>/dev/null) || return 0

  [[ -z "$staged_files" ]] && return 0

  local allowlist
  allowlist=$(load_allowlist)

  local total=0 critical=0 high=0

  while IFS= read -r file; do
    [[ -z "$file" || ! -f "$file" ]] && continue
    is_binary_file "$file" && continue

    local findings
    findings=$(scan_file "$file" "$allowlist") || true

    if [[ -n "$findings" ]]; then
      while IFS=$'\t' read -r f_file f_line f_severity f_service f_desc f_redacted; do
        [[ -z "$f_file" ]] && continue
        ((total++)) || true
        case "$f_severity" in
          critical) ((critical++)) || true ;;
          high)     ((high++)) || true ;;
        esac

        local icon sev_display
        icon=$(severity_icon "$f_severity")
        sev_display=$(severity_color "$f_severity")

        echo -e "  $icon ${BOLD}$f_file${NC}:${CYAN}$f_line${NC}"
        echo -e "    $sev_display  $f_desc"
        echo -e "    Match: ${DIM}$f_redacted${NC}"
      done <<< "$findings"
    fi
  done <<< "$staged_files"

  if [[ "$critical" -gt 0 || "$high" -gt 0 ]]; then
    echo ""
    echo -e "${RED}${BOLD}Commit blocked: $total secret(s) detected ($critical critical, $high high)${NC}"
    return 1
  fi

  if [[ "$total" -gt 0 ]]; then
    echo ""
    echo -e "${YELLOW}[EnvGuard]${NC} $total potential secret(s) found (low/medium severity). Commit allowed."
  fi

  return 0
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)