Skill: teeclaw/openclaw-credential-manager (version 2.0.0)

--- name: credential-manager description: MANDATORY security foundation for OpenClaw. Consolidate scattered API keys and credentials into a secure .env file with proper permissions. Includes GPG encryption for high-value secrets, credential rotation tracking, deep scanning, and backup hardening. Use when setting up OpenClaw, migrating credentials, auditing security, or enforcing the .env standard. This is not optional — centralized credential management is a core requirement for secure OpenClaw deployments. --- # Credential Manager **STATUS: MANDATORY SECURITY FOUNDATION** Consolidate scattered API keys and credentials into a secure, centralized `.env` file. ## ⚠️ This Is Not Optional Centralized `.env` credential management is a **core requirement** for OpenClaw security. If your credentials are scattered across multiple files, **stop and consolidate them now**. **THE RULE:** All credentials MUST be in `~/.openclaw/.env` ONLY. No workspace, no skills, no scripts directories. See: - [CORE-PRINCIPLE.md](CORE-PRINCIPLE.md) - Why this is non-negotiable - [CONSOLIDATION-RULE.md](CONSOLIDATION-RULE.md) - The single source principle ## The Foundation **Every OpenClaw deployment MUST have:** ``` ~/.openclaw/.env (mode 600) ``` This is your single source of truth for all credentials. No exceptions. **Why?** - Single location = easier to secure - File mode 600 = only you can read - Git-ignored = won't accidentally commit - Validated format = catches errors - Audit trail = know what changed Scattered credentials = scattered attack surface. This skill fixes that. ## What This Skill Does 1. **Scans** for credentials in common locations (including deep scan for hardcoded secrets) 2. **Backs up** existing credential files (timestamped, mode 600) 3. **Consolidates** into `~/.openclaw/.env` 4. **Secures** with proper permissions (600 files, 700 directories) 5. **Validates** security, format, and entropy 6. **Encrypts** high-value secrets with GPG (wallet keys, private keys, mnemonics) 7. **Tracks** credential rotation schedules 8. **Enforces** best practices via fail-fast checks 9. **Cleans up** old files after migration ## Detection Parameters The skill automatically detects credentials by scanning for: **File Patterns:** - `~/.config/*/credentials.json` — Service config directories - `~/.config/*/*.credentials.json` — Nested credential files - `~/.openclaw/*.json` — Credential files in OpenClaw root - `~/.openclaw/*-credentials*` — Named credential files (e.g., farcaster-credentials.json) - `~/.openclaw/workspace/memory/*-creds.json` — Memory credential files - `~/.openclaw/workspace/memory/*credentials*.json` — Memory credential files - `~/.openclaw/workspace/.env` — Workspace env files - `~/.openclaw/workspace/*/.env` — Subdirectory env files - `~/.openclaw/workspace/skills/*/.env` — Skill env files - `~/.local/share/*/credentials.json` — Local share directories **Sensitive Key Patterns:** - API keys, access tokens, bearer tokens - Secrets, passwords, passphrases - OAuth consumer keys - Private keys, signing keys, wallet keys - Mnemonics and seed phrases **Deep Scan (--deep flag):** - Greps `.sh`, `.js`, `.py`, `.mjs`, `.ts` files for hardcoded secrets - Detects high-entropy strings matching common key prefixes (`sk_`, `pk_`, `Bearer`, `0x` + 64 hex) - Excludes `node_modules/`, `.git/` - Reports file, line number, and key pattern matched **Security Checks:** - File permissions (must be `600` for files, `700` for directories) - Backup permissions (must be `600` for backup files, `700` for backup dirs) - Git-ignore protection - Format validation (allows quoted values with spaces) - Entropy analysis (flags suspiciously low-entropy secrets) - Private key detection (flags `0x` + 64 hex char values) - Mnemonic detection (flags 12/24 word values) - Symlink detection (validates symlinked .env targets) ## Quick Start ### Full Migration (Recommended) ```bash # Scan for credentials ./scripts/scan.py # Deep scan (includes hardcoded secrets in scripts) ./scripts/scan.py --deep # Review and consolidate ./scripts/consolidate.py # Validate security ./scripts/validate.py # Encrypt high-value secrets ./scripts/encrypt.py --keys MAIN_WALLET_PRIVATE_KEY,CUSTODY_PRIVATE_KEY # Check rotation status ./scripts/rotation-check.py ``` ### Individual Operations ```bash # Scan only ./scripts/scan.py # Consolidate specific service ./scripts/consolidate.py --service x # Backup without removing ./scripts/consolidate.py --backup-only # Clean up old files ./scripts/cleanup.py --confirm ``` ## Common Credential Locations The skill scans these locations: ``` ~/.config/*/credentials.json ~/.openclaw/*.json ~/.openclaw/*-credentials* ~/.openclaw/workspace/memory/*-creds.json ~/.openclaw/workspace/memory/*credentials*.json ~/.openclaw/workspace/*/.env ~/.openclaw/workspace/skills/*/.env ~/.env (if exists, merges) ``` ## Security Features ✅ **File permissions:** Sets `.env` to mode 600 (owner only) ✅ **Directory permissions:** Sets backup dirs to mode 700 (owner only) ✅ **Backup permissions:** Sets backup files to mode 600 (owner only) ✅ **Git protection:** Creates/updates `.gitignore` ✅ **Backups:** Timestamped backups before changes (secured) ✅ **Validation:** Checks format, permissions, entropy, and duplicates ✅ **Template:** Creates `.env.example` (safe to share) ✅ **GPG encryption:** Encrypts high-value secrets at rest ✅ **Rotation tracking:** Warns when credentials need rotation ✅ **Deep scan:** Detects hardcoded secrets in source files ✅ **Symlink-aware:** Validates symlinked .env targets ## Output Structure After migration: ``` ~/.openclaw/ ├── .env # All credentials (secure, mode 600) ├── .env.secrets.gpg # GPG-encrypted high-value keys (mode 600) ├── .env.meta # Rotation metadata (mode 600) ├── .env.example # Template (safe to share) ├── .gitignore # Protects .env and .env.secrets.gpg └── backups/ # (mode 700) └── credentials-old-YYYYMMDD/ # (mode 700) └── *.bak # Backup files (mode 600) ``` ## GPG Encryption for High-Value Secrets Private keys, wallet keys, and mnemonics should **never** exist as plaintext on disk. Use GPG encryption for these. ### Setup GPG ```bash # First-time setup (generates OpenClaw GPG key, configures agent cache) ./scripts/setup-gpg.sh ``` ### Encrypt High-Value Keys ```bash # Encrypt specific keys (moves them from .env to .env.secrets.gpg) ./scripts/encrypt.py --keys MAIN_WALLET_PRIVATE_KEY,CUSTODY_PRIVATE_KEY,SIGNER_PRIVATE_KEY # The .env will contain placeholders: # MAIN_WALLET_PRIVATE_KEY=GPG:MAIN_WALLET_PRIVATE_KEY ``` ### How Scripts Access Encrypted Keys The `enforce.py` module handles this transparently: ```python from enforce import get_credential # Works for both plaintext and GPG-encrypted keys key = get_credential('MAIN_WALLET_PRIVATE_KEY') # If value starts with "GPG:", decrypts from .env.secrets.gpg automatically ``` ### GPG Agent Caching On headless servers (VPS), the GPG agent caches the passphrase: - Default cache TTL: 8 hours - Configurable via `setup-gpg.sh` - Passphrase required once after reboot, then cached ### What to Encrypt | Key Type | Encrypt? | Why | |----------|----------|-----| | Wallet private keys | ✅ Yes | Controls funds | | Custody/signer private keys | ✅ Yes | Controls identity | | Mnemonics / seed phrases | ✅ Yes | Master recovery | | API keys (services) | ❌ No | Revocable, low damage | | Agent IDs, names, URLs | ❌ No | Not secrets | ## Credential Rotation Tracking ### Setup Rotation Metadata ```bash # Initialize rotation tracking for all keys ./scripts/rotation-check.py --init ``` Creates `~/.openclaw/.env.meta`: ```json { "MAIN_WALLET_PRIVATE_KEY": { "created": "2026-01-15", "lastRotated": null, "rotationDays": 90, "risk": "critical" }, "MOLTBOOK_API_KEY": { "created": "2026-02-04", "lastRotated": null, "rotationDays": 180, "risk": "low" } } ``` ### Check Rotation Status ```bash # Check which keys need rotation ./scripts/rotation-check.py # Output: # 🔴 MAIN_WALLET_PRIVATE_KEY: 26 days old (critical, rotate every 90 days) # ✅ MOLTBOOK_API_KEY: 7 days old (low, rotate every 180 days) ``` ### Rotation Schedules | Risk Level | Rotation Period | Examples | |------------|----------------|----------| | Critical | 90 days | Wallet keys, private keys | | Standard | 180 days | API keys for paid services | | Low | 365 days | Free-tier API keys, agent IDs | ### Add to Heartbeat (Optional) Add rotation checks to `HEARTBEAT.md` for periodic monitoring: ```markdown ## Credential Rotation (weekly) If 7+ days since last rotation check: 1. Run: ./scripts/rotation-check.py 2. If any keys overdue: notify human 3. Update lastRotationCheck timestamp ``` ## Supported Services Common services auto-detected: - **X (Twitter):** OAuth 1.0a credentials - **Farcaster:** Custody keys, signer keys, FID credentials - **Molten:** Agent intent matching - **Moltbook:** Agent social network - **Botchan/4claw:** Net Protocol - **OpenAI, Anthropic, Google:** AI providers - **GitHub, GitLab:** Code hosting - **Coinbase/CDP:** Crypto wallet credentials - **Generic:** `API_KEY`, `*_TOKEN`, `*_SECRET` patterns See [references/supported-services.md](references/supported-services.md) for full list. ## Scripts All scripts support `--help` for detailed usage. ### scan.py ```bash # Scan and report ./scripts/scan.py # Deep scan (includes hardcoded secrets in scripts) ./scripts/scan.py --deep # Include custom paths ./scripts/scan.py --paths ~/.myapp/config ~/.local/share/creds # JSON output ./scripts/scan.py --format json ``` ### consolidate.py ```bash # Interactive mode (prompts before changes) ./scripts/consolidate.py # Auto-confirm (no prompts) ./scripts/consolidate.py --yes # Backup only ./scripts/consolidate.py --backup-only # Specific service ./scripts/consolidate.py --service molten ``` ### validate.py ```bash # Full validation (permissions, format, entropy, security) ./scripts/validate.py # Check permissions only ./scripts/validate.py --check permissions # Fix issues automatically ./scripts/validate.py --fix ``` ### encrypt.py ```bash # Encrypt specific high-value keys ./scripts/encrypt.py --keys MAIN_WALLET_PRIVATE_KEY,CUSTODY_PRIVATE_KEY # List currently encrypted keys ./scripts/encrypt.py --list # Decrypt (move back to plaintext .env) ./scripts/encrypt.py --decrypt --keys MAIN_WALLET_PRIVATE_KEY ``` ### rotation-check.py ```bash # Check rotation status ./scripts/rotation-check.py # Initialize tracking for all keys ./scripts/rotation-check.py --init # Record a rotation ./scripts/rotation-check.py --rotated MOLTBOOK_API_KEY ``` ### setup-gpg.sh ```bash # First-time GPG setup for OpenClaw ./scripts/setup-gpg.sh # Configure cache timeout (hours) ./scripts/setup-gpg.sh --cache-hours 12 ``` ### cleanup.py ```bash # Dry run (shows what would be deleted) ./scripts/cleanup.py # Actually delete old files ./scripts/cleanup.py --confirm # Keep backups ./scripts/cleanup.py --confirm --keep-backups ``` ## Migration Workflow This is the exact step-by-step flow, tested and verified on a live OpenClaw deployment. ### Step 1: Scan for Scattered Credentials ```bash cd /path/to/openclaw/skills/credential-manager # Basic scan — finds credential files by path patterns ./scripts/scan.py # Deep scan — also greps source files for hardcoded secrets ./scripts/scan.py --deep ``` **What to look for in output:** - ⚠️ files with mode != 600 (insecure permissions) - Symlinked `.env` files (should point to main `~/.openclaw/.env`) - JSON credential files outside `~/.openclaw/.env` - Deep scan hits on hardcoded keys in scripts **Example output:** ``` ⚠️ /home/user/.openclaw/farcaster-credentials.json Type: json Keys: custodyPrivateKey, signerPrivateKey, ... Mode: 644 ⚠️ Should be 600 for security ✅ /home/user/.openclaw/.env Type: env Keys: API_KEY, X_CONSUMER_KEY, ... Mode: 600 ``` ### Step 2: Consolidate into .env ```bash ./scripts/consolidate.py ``` **Interactive flow:** 1. Script scans and lists all credential files found 2. Backs up existing `.env` to `~/.openclaw/backups/credentials-old-YYYYMMDD/` 3. Loads existing `.env` keys 4. Processes each credential file: - Auto-detects service (x, farcaster, moltbook, molten, etc.) - Normalizes key names (e.g., `custodyPrivateKey` → `FARCASTER_CUSTODY_PRIVATE_KEY`) - Shows mapping: `key → ENV_KEY` 5. Asks for confirmation: `Proceed? [y/N]` 6. Writes merged `.env` (mode 600) 7. Creates `.env.example` template (safe to share) 8. Updates `.gitignore` **For credentials not auto-detected** (e.g., nested JSON like `farcaster-credentials.json` with multiple accounts), manually add to `.env`: ```bash cat >> ~/.openclaw/.env << 'EOF' # FARCASTER (Active: mr-teeclaw, FID 2700953) FARCASTER_FID=2700953 FARCASTER_FNAME=mr-teeclaw FARCASTER_CUSTODY_ADDRESS=0x... FARCASTER_CUSTODY_PRIVATE_KEY=0x... FARCASTER_SIGNER_PUBLIC_KEY=... FARCASTER_SIGNER_PRIVATE_KEY=... # FARCASTER LEGACY (teeclaw, FID 2684290) FARCASTER_LEGACY_FID=2684290 FARCASTER_LEGACY_CUSTODY_ADDRESS=0x... FARCASTER_LEGACY_CUSTODY_PRIVATE_KEY=0x... FARCASTER_LEGACY_SIGNER_PUBLIC_KEY=... FARCASTER_LEGACY_SIGNER_PRIVATE_KEY=... EOF chmod 600 ~/.openclaw/.env ``` ### Step 3: Validate ```bash ./scripts/validate.py ``` **Checks performed:** - ✅ `.env` permissions (must be 600) - ✅ `.gitignore` coverage - ✅ Format validation (key format, quoting, duplicates) - ✅ Security analysis: - Detects plaintext private keys (`0x` + 64 hex chars) → recommends GPG - Detects mnemonic/seed phrases (12/24 word values) → recommends GPG - Entropy analysis on SECRET/PRIVATE_KEY/PASSWORD fields - Flags weak/placeholder values - ✅ Backup permissions (files must be 600, directories 700) **Fix issues automatically:** ```bash ./scripts/validate.py --fix ``` This fixes: file permissions, directory permissions, backup permissions, gitignore. It does NOT auto-fix format issues or encrypt keys — those require manual action. ### Step 4: Setup GPG and Encrypt Private Keys ```bash # First-time GPG setup (configures agent cache, tests encrypt/decrypt) ./scripts/setup-gpg.sh # Optional: --cache-hours 12 (default: 8) ``` **Encrypt high-value keys:** ```bash # Encrypt wallet + Farcaster private keys ./scripts/encrypt.py --keys MAIN_WALLET_PRIVATE_KEY,FARCASTER_CUSTODY_PRIVATE_KEY,FARCASTER_SIGNER_PRIVATE_KEY,FARCASTER_LEGACY_CUSTODY_PRIVATE_KEY,FARCASTER_LEGACY_SIGNER_PRIVATE_KEY ``` **What happens:** 1. Prompts for a GPG passphrase (or reads `OPENCLAW_GPG_PASSPHRASE` env var) 2. Extracts specified key values from `.env` 3. Stores them encrypted in `~/.openclaw/.env.secrets.gpg` (AES256, mode 600) 4. Replaces `.env` values with `GPG:KEY_NAME` placeholders 5. Scripts using `get_credential()` or `_load_cred()` decrypt transparently **Save passphrase to .env for automated decryption:** ```bash echo [REDACTED_SECRET] >> ~/.openclaw/.env chmod 600 ~/.openclaw/.env ``` **Verify encryption:** ```bash # Check .env has GPG placeholders grep "GPG:" ~/.openclaw/.env # List all encrypted keys ./scripts/encrypt.py --list ``` ### Step 5: Initialize Rotation Tracking ```bash ./scripts/rotation-check.py --init ``` **Auto-classifies all keys by risk:** - **Critical** (90-day rotation): `*PRIVATE_KEY`, `*MNEMONIC`, `*SEED`, `*WALLET_KEY`, `*CUSTODY*`, `*SIGNER*` - **Standard** (180-day rotation): `*API_KEY`, `*SECRET`, `*TOKEN`, `*BEARER`, `*CONSUMER*`, `*ACCESS*` - **Low** (365-day rotation): Everything else Creates `~/.openclaw/.env.meta` (mode 600) with creation dates and rotation schedules. **Check rotation status anytime:** ```bash ./scripts/rotation-check.py ``` ### Step 6: Cleanup Old Credential Files ```bash # Dry run first — see what would be deleted ./scripts/cleanup.py # Actually delete (prompts for 'DELETE' confirmation) ./scripts/cleanup.py --confirm ``` **Also manually remove migrated files not caught by the scanner:** ```bash # Example: farcaster-credentials.json was manually migrated cp ~/.openclaw/farcaster-credentials.json ~/.openclaw/backups/credentials-old-YYYYMMDD/farcaster-credentials.json.bak chmod 600 ~/.openclaw/backups/credentials-old-YYYYMMDD/farcaster-credentials.json.bak rm ~/.openclaw/farcaster-credentials.json ``` ### Step 7: Update Scripts That Referenced Old Files Any scripts that loaded from JSON credential files or hardcoded paths need updating. **Pattern — Bash scripts:** ```bash # OLD (insecure): FARCASTER_CREDS="/home/user/.openclaw/farcaster-credentials.json" fid=$(jq -r '.fid' "$FARCASTER_CREDS") private_key=$(jq -r '.custodyPrivateKey' "$FARCASTER_CREDS") # NEW (secure, GPG-aware): ENV_FILE="$HOME/.openclaw/.env" _load_cred() { local key="$1" local value value=$(grep "^${key}=" "$ENV_FILE" | head -1 | cut -d= -f2-) if [[ "$value" == GPG:* ]]; then local gpg_key="${value#GPG:}" local passphrase="${OPENCLAW_GPG_PASSPHRASE:-}" if [ -n "$passphrase" ]; then value=$(echo "$passphrase" | gpg -d --batch --quiet --passphrase-fd 0 "$HOME/.openclaw/.env.secrets.gpg" | python3 -c "import json,sys; print(json.load(sys.stdin).get('$gpg_key',''))") else value=$(gpg -d --batch --quiet "$HOME/.openclaw/.env.secrets.gpg" | python3 -c "import json,sys; print(json.load(sys.stdin).get('$gpg_key',''))") fi fi echo "$value" } fid=$(_load_cred "FARCASTER_FID") private_key=$(_load_cred "FARCASTER_CUSTODY_PRIVATE_KEY") ``` **Pattern — Node.js scripts:** ```javascript // OLD (insecure): const creds = JSON.parse(fs.readFileSync('~/.openclaw/farcaster-credentials.json')); const privateKey = creds.custodyPrivateKey; // NEW (secure, GPG-aware): const ENV_PATH = path.join(os.homedir(), '.openclaw/.env'); const SECRETS_PATH = path.join(os.homedir(), '.openclaw/.env.secrets.gpg'); function loadCred(key) { const content = fs.readFileSync(ENV_PATH, 'utf8'); for (const line of content.split('\n')) { if (line.startsWith(key + '=')) { let value = line.slice(key.length + 1).trim(); if (value.startsWith('GPG:')) { const { execSync } = require('child_process'); const passphrase = process.env.OPENCLAW_GPG_PASSPHRASE || ''; const cmd = passphrase ? `echo "${passphrase}" | gpg -d --batch --quiet --passphrase-fd 0 "${SECRETS_PATH}"` : `gpg -d --batch --quiet "${SECRETS_PATH}"`; const secrets = JSON.parse(execSync(cmd, { encoding: 'utf8' })); return secrets[value.slice(4)] || ''; } return value; } } return ''; } const privateKey = loadCred('FARCASTER_CUSTODY_PRIVATE_KEY'); ``` **Pattern — Python scripts:** ```python # Use the enforce module (recommended): import sys from pathlib import Path sys.path.insert(0, str(Path.home() / [REDACTED_SECRET])) from enforce import get_credential private_key = get_credential('FARCASTER_CUSTODY_PRIVATE_KEY') # Auto-decrypts GPG ``` ### Step 8: Final Validation ```bash # Run full validation — should show all green ./scripts/validate.py # Verify encrypted keys ./scripts/encrypt.py --list # Check rotation status ./scripts/rotation-check.py # Test a script that uses credentials bash /path/to/your/script.sh ``` **Expected final state:** ``` ~/.openclaw/ ├── .env # All credentials (mode 600, private keys = GPG:*) ├── .env.secrets.gpg # GPG-encrypted private keys (mode 600) ├── .env.meta # Rotation tracking metadata (mode 600) ├── .env.example # Template (safe to share) ├── .gitignore # Protects .env, .env.secrets.gpg, .env.meta └── backups/ # (mode 700) └── credentials-old-YYYYMMDD/ # (mode 700) └── *.bak # Backup files (mode 600) ``` ## For Skill Developers: Enforce This Standard Other OpenClaw skills MUST validate credentials are secure before using them: ### Python Skills ```python #!/usr/bin/env python3 import sys from pathlib import Path # Add credential-manager scripts to path sys.path.insert(0, str(Path.home() / '.openclaw/skills/credential-manager/scripts')) # Enforce secure .env (exits if not compliant) from enforce import require_secure_env, get_credential require_secure_env() # Now safe to load credentials (handles GPG-encrypted keys transparently) [REDACTED_SECRET]'SERVICE_API_KEY') wallet_key = get_credential('MAIN_WALLET_PRIVATE_KEY') # Auto-decrypts from GPG ``` ### Bash Skills ```bash #!/usr/bin/env bash set -euo pipefail # Validate .env exists and is secure if ! python3 ~/.openclaw/skills/credential-manager/scripts/enforce.py; then exit 1 fi # Now safe to load source ~/.openclaw/.env ``` **This creates a fail-fast system:** If credentials aren't properly secured, skills refuse to run. Users are forced to fix it. ## Loading Credentials After migration, load from `.env`: ### Python ```python import os from pathlib import Path # Load .env env_file = Path.home() / '.openclaw' / '.env' with open(env_file) as f: for line in f: if '=' in line and not line.strip().startswith('#'): key, val = line.strip().split('=', 1) os.environ[key] = val # Use credentials [REDACTED_SECRET]'SERVICE_API_KEY') ``` ### Bash ```bash # Load .env set -a source ~/.openclaw/.env set +a # Use credentials echo "$SERVICE_API_KEY" ``` ### Using Existing Loaders If you migrated using OpenClaw scripts: ```python from load_credentials import get_credentials creds = get_credentials('x') ``` ## Adding New Credentials Edit `~/.openclaw/.env`: ```bash # Add new service NEW_SERVICE_API_KEY=your_key_here NEW_SERVICE_SECRET=your_secret_here ``` Update template too: ```bash # Edit .env.example NEW_SERVICE_API_KEY=your_key_here NEW_SERVICE_SECRET=your_secret_here ``` If the new credential is high-value (private key, wallet key): ```bash # Add to .env first, then encrypt ./scripts/encrypt.py --keys NEW_SERVICE_PRIVATE_KEY ``` ## Security Best Practices See [references/security.md](references/security.md) for detailed security guidelines. **Quick checklist:** - ✅ `.env` has 600 permissions - ✅ `.env` is git-ignored - ✅ Backup files have 600 permissions - ✅ Backup directories have 700 permissions - ✅ No credentials in code or logs (use `--deep` scan to verify) - ✅ Private keys encrypted with GPG - ✅ Rotation schedule established and tracked - ✅ Symlinked .env files point to the main .env only - ✅ No credentials in shell history (use `source`, not `export KEY=val`) ## Rollback If something goes wrong: ```bash # Find your backup ls -la ~/.openclaw/backups/ # Restore specific file cp ~/.openclaw/backups/credentials-old-YYYYMMDD/x-credentials.json.bak \ ~/.config/x/credentials.json # Decrypt GPG secrets back to plaintext ./scripts/encrypt.py --decrypt --keys MAIN_WALLET_PRIVATE_KEY ``` ## Notes - **Non-destructive by default:** Original files backed up before removal - **Idempotent:** Safe to run multiple times - **Extensible:** Add custom credential patterns in scripts - **Secure:** Never logs full credentials, only metadata - **GPG-aware:** Transparently handles encrypted and plaintext credentials - **Backup-hardened:** All backups secured with proper permissions - **Symlink-aware:** Detects and validates symlinked credential files

path: CHANGELOG.md
content: # Changelog

## Version 2.0.0 (2026-02-11)

### 🔐 GPG Encryption, Deep Scanning, Rotation Tracking & Backup Hardening

**Major security upgrade.** Private keys and wallet secrets are now GPG-encrypted at rest. Scanner catches hardcoded secrets in source files. All backup permissions hardened. Credential rotation is tracked with risk-based schedules.

### Added

**New scripts:**

- **`encrypt.py`** — GPG encrypt/decrypt high-value secrets
  - Moves private keys from plaintext `.env` to `~/.openclaw/.env.secrets.gpg` (AES256)
  - Replaces values in `.env` with `GPG:KEY_NAME` placeholders
  - `--list` shows currently encrypted keys
  - `--decrypt --keys KEY` reverses encryption
  - Supports passphrase via `OPENCLAW_GPG_PASSPHRASE` env var or interactive prompt

- **`setup-gpg.sh`** — First-time GPG configuration
  - Checks GPG installation
  - Configures gpg-agent cache (default 8h, `--cache-hours N`)
  - Enables loopback pinentry for headless servers
  - Runs encrypt/decrypt test cycle

- **`rotation-check.py`** — Credential rotation tracking
  - `--init` creates `~/.openclaw/.env.meta` with auto-classified risk levels
  - Critical keys (private keys, mnemonics): 90-day rotation
  - Standard keys (API keys, tokens): 180-day rotation
  - Low-risk keys: 365-day rotation
  - `--rotated KEY` records a rotation event
  - Shows overdue/upcoming/OK status with color-coded output

**New scan capabilities:**

- **Deep scan** (`--deep` flag): Greps `.sh`, `.js`, `.py`, `.mjs`, `.ts` files for:
  - API key prefixes (`sk_`, `pk_`, `Bearer`)
  - Possible private keys (`0x` + 64 hex chars)
  - Hardcoded credential assignments
  - Mnemonic/seed phrases
  - Excludes `node_modules/`, `.git/`

- **Expanded scan patterns:**
  - `~/.openclaw/*.json` (catches `farcaster-credentials.json` etc.)
  - `~/.openclaw/*-credentials*`
  - `~/.openclaw/workspace/*/.env`
  - `~/.openclaw/workspace/*/repo/.env`

- **Symlink detection:** Reports symlinked `.env` files and validates they point to the main `.env`

**New validation checks:**

- **Entropy analysis:** Flags low-entropy values for SECRET/PRIVATE_KEY/PASSWORD fields
- **Private key detection:** Flags `0x` + 64 hex char values (recommends GPG encryption)
- **Mnemonic detection:** Flags 12/24 space-separated word values
- **Backup permission audit:** Checks all backup files (mode 600) and directories (mode 700)
- **`--fix` now works:** Actually fixes permissions, backup permissions, directory permissions, gitignore

**GPG-aware credential loading:**

- `enforce.py` `get_credential()` transparently decrypts `GPG:` prefixed values
- `is_gpg_available()` helper for checking GPG status
- Reports GPG encryption status when run directly

### Changed

- **`consolidate.py`:**
  - Backup files now created with mode 600 (was inheriting source permissions)
  - Backup directories created with mode 700 (was 755)
  - Parent backup directory also secured to 700
  - Added Farcaster to `SERVICE_MAPPINGS` (custodyPrivateKey, signerPrivateKey, etc.)
  - `.gitignore` now also covers `.env.secrets.gpg` and `.env.meta`
  - Skips symlinks that point to the main `.env`
  - Flattens nested JSON objects to compact JSON strings

- **`validate.py`:**
  - Fixed quote contradiction: now allows double-quoted values with spaces
  - Removed false "Quotes not needed" check
  - Added backup security checks as a new validation category
  - `--fix` flag now properly fixes backup permissions
  - `check_security()` upgraded from naive pattern matching to entropy + pattern analysis

- **`scan.py`:**
  - Added 4 new credential path patterns
  - Symlink detection with target validation
  - `--deep` flag for source file scanning
  - Shannon entropy calculation utility

- **`enforce.py`:**
  - `get_credential()` now handles `GPG:` prefixed values
  - Decrypts from `.env.secrets.gpg` using gpg-agent cache or passphrase
  - Reports GPG encryption status

- **`SKILL.md`:**
  - Complete rewrite of Migration Workflow with exact step-by-step flow
  - Added GPG Encryption section with setup, usage, and what-to-encrypt guide
  - Added Credential Rotation Tracking section
  - Added Deep Scan documentation
  - Added Bash/Node.js/Python patterns for GPG-aware credential loading
  - Updated security checklist
  - Added Farcaster to supported services

### Security Fixes

- 🔴 **Backup files** were created with mode 644 (world-readable) — now mode 600
- 🔴 **Backup directories** were created with mode 755 — now mode 700
- 🔴 **Scanner blind spot:** `~/.openclaw/*.json` was not scanned — now included
- 🔴 **farcaster-credentials.json** with private keys had mode 644 — detected and flagged
- 🟡 **validate.py** contradicted itself on quotes — fixed
- 🟡 **check_security()** only checked for "password123" — now does entropy + pattern analysis

### Migration Story

This update was built during a live security audit that discovered:
1. `farcaster-credentials.json` with private keys at mode 644 (world-readable!)
2. All backup files at mode 644 (should be 600)
3. All backup directories at mode 755 (should be 700)
4. Wallet private key sitting in plaintext `.env`
5. Scanner couldn't detect `~/.openclaw/*.json` files
6. `validate.py` had conflicting quote validation rules

After running the upgraded skill:
- ✅ 5 private keys GPG-encrypted (wallet + 4 Farcaster keys)
- ✅ All backup permissions fixed (12 files/dirs)
- ✅ `farcaster-credentials.json` migrated to `.env` and deleted
- ✅ 57 keys tracked with risk-based rotation schedules
- ✅ 4 scripts updated to use `.env` with GPG decryption
- ✅ All validation checks pass

### Technical Details

**New files:** 3 scripts (`encrypt.py`, `setup-gpg.sh`, `rotation-check.py`)
**Modified files:** 5 (`scan.py`, `consolidate.py`, `validate.py`, `enforce.py`, `SKILL.md`)
**New file types:** `.env.secrets.gpg` (encrypted), `.env.meta` (rotation metadata)
**Package size:** ~65 KB

---

## Version 1.3.0 (2026-02-07)

### 🎯 Consolidation Rule - Single Source Enforcement

**Major update:** Formal enforcement of the single source principle — all credentials MUST be in `~/.openclaw/.env` ONLY.

### Added

**CONSOLIDATION-RULE.md** - New comprehensive documentation:
- The single source principle explained
- Why root-only (no workspace, skills, scripts .env files)
- Enforcement workflow (scan → consolidate → cleanup → validate)
- Security rationale (one file to secure, audit, backup)
- Developer guidance (how to load from root .env)
- Exception handling (node_modules .env files are harmless)

**Enhanced scan.py detection patterns:**
- `~/.openclaw/workspace/.env`
- `~/.openclaw/workspace/.env.*`
- `~/.openclaw/workspace/skills/*/.env`
- `~/.openclaw/workspace/skills/*/repo/.env`
- `~/.openclaw/workspace/scripts/.env`

**Enhanced cleanup.py:**
- Updated header to explicitly mention rule enforcement
- Removes scattered .env files from workspace/skills/scripts
- Preserves backups for safety

**Updated SKILL.md:**
- Prominently references CONSOLIDATION-RULE.md
- Added "THE RULE" section upfront
- Emphasizes root-only requirement

### Technical Details

**New files:** 1 (CONSOLIDATION-RULE.md)
**Modified files:** 4 (scan.py, cleanup.py, SKILL.md, README.md)
**Detection patterns:** +5 workspace-specific patterns

---

## Version 1.2.0 (2026-02-06)

### 🔐 Crypto-Specific Credential Detection

**Enhanced detection patterns** for blockchain and cryptocurrency credentials.

### Added

- `private_key` / `private-key` - Wallet private keys
- `passphrase` - Seed passphrases
- `mnemonic` - BIP39 recovery phrases
- `seed_phrase` / `seed-phrase` - Wallet seed phrases
- `signing_key` / `signing-key` - Transaction signing keys
- `wallet_key` / `wallet-key` - Wallet access keys

---

## Version 1.1.0 (2026-02-05)

### 🔒 SECURITY FOUNDATION UPDATE

- Added `CORE-PRINCIPLE.md` establishing mandatory security standard
- Added `enforce.py` with `require_secure_env()` and `get_credential()`
- Repositioned as mandatory core infrastructure

---

## Version 1.0.0 (2026-02-05)

Initial release with core functionality:
- Credential scanning
- .env consolidation
- Security validation
- Old file cleanup
- Comprehensive documentation


path: CONSOLIDATION-RULE.md
content: # Credential Consolidation Rule

**RULE: All credentials MUST be consolidated to `/home/phan_harry/.openclaw/.env`**

## The Single Source Principle

There is exactly **ONE** location for all OpenClaw credentials:

```
~/.openclaw/.env
```

**No exceptions.** Not workspace, not skills, not scripts. Root only.

## Why Root Only?

1. **Security:** One file to secure (mode 600), one file to audit
2. **Simplicity:** Scripts know exactly where to look
3. **Git safety:** Single .gitignore rule protects everything
4. **Backup:** One file to backup/restore
5. **Portability:** Copy one file, move entire credential set

## What Gets Consolidated

**ALL of these must be merged into root .env:**

- `~/.openclaw/workspace/.env` ❌ → Root
- `~/.openclaw/workspace/skills/*/.env` ❌ → Root
- `~/.openclaw/workspace/skills/*/repo/.env` ❌ → Root
- `~/.openclaw/workspace/scripts/.env` ❌ → Root
- `~/.config/*/credentials.json` ❌ → Root
- Any scattered API key files ❌ → Root

## Enforcement

The credential-manager skill enforces this rule:

1. **Scan:** Detects ALL .env files and credential files
2. **Consolidate:** Merges everything into `~/.openclaw/.env`
3. **Cleanup:** Removes scattered files (after backup)
4. **Validate:** Ensures no scattered files remain

## Running Consolidation

```bash
cd ~/openclaw/skills/credential-manager

# Scan for scattered credentials
./scripts/scan.py

# Consolidate to root (with backup)
./scripts/consolidate.py --yes

# Clean up scattered files
./scripts/cleanup.py --confirm

# Validate security
./scripts/validate.py
```

## After Consolidation

✅ **Only these files should exist:**

- `~/.openclaw/.env` (mode 600) - Your credentials
- `~/.openclaw/.env.example` - Template (safe to share)
- `~/.openclaw/backups/credentials-old-YYYYMMDD/` - Backups

❌ **These should NOT exist:**

- `~/.openclaw/workspace/.env`
- `~/.openclaw/workspace/skills/*/.env`
- Any other .env files outside node_modules

## For Skill Developers

**DO NOT create .env files in your skill directories.**

Load credentials from root:

```bash
#!/bin/bash
# Load from root .env
source ~/.openclaw/.env

# Use credentials
echo "$SERVICE_API_KEY"
```

```python
#!/usr/bin/env python3
# Load from root .env
from pathlib import Path
env_file = Path.home() / '.openclaw' / '.env'
# ... load and use
```

## Exception: node_modules

.env files inside `node_modules/` are package defaults (e.g., bottleneck's Redis config). These are harmless and ignored by the scanner.

## Rationale

Scattered credentials create scattered attack surface. A single .env file:
- Is easier to secure (one chmod 600)
- Is easier to audit (one file to check)
- Is easier to backup (one file to save)
- Is easier to gitignore (one rule)
- Is easier to rotate (change in one place)

**Consolidation is not optional. It's a core security requirement.**


path: CORE-PRINCIPLE.md
content: # Core Principle: Centralized Credential Management

## This Is Not Optional

`.env` credential consolidation is a **mandatory security foundation** for OpenClaw deployments.

## Why This Matters

### Security Risk: Scattered Credentials

When credentials live in multiple locations:
```
~/.config/x/credentials.json          (mode 644 - WORLD READABLE!)
~/.config/service/api_keys.json       (no git protection)
~/project/.env                         (accidentally committed)
~/.bashrc                              (exported in shell history)
```

**Result:** Attack surface grows. One mistake compromises everything.

### The Foundation: Single Source of Truth

```
~/.openclaw/.env (mode 600)
```

**One file. One permission. One location to secure.**

## Non-Negotiable Requirements

### 1. File Permissions
```bash
chmod 600 ~/.openclaw/.env
```
Owner read/write ONLY. No group. No world.

### 2. Git Protection
```gitignore
.env
*.env
!.env.example
```
Never commit credentials. Ever.

### 3. Backup Strategy
Encrypted backups only. Or no backups.

### 4. Access Control
Minimize who/what can read `.env`:
- Your user account
- Processes you explicitly run
- Nothing else

## Why OpenClaw Enforces This

**Agent systems are privileged by design.**

An AI agent needs credentials to:
- Post on your behalf
- Execute transactions
- Access private data
- Control infrastructure

**If credentials leak, the agent becomes a weapon.**

## The Trade-Off

**Convenience vs Security:** We choose security.

Yes, scattered JSON files are "easier" to set up initially. But they're harder to:
- Audit
- Rotate
- Protect
- Monitor

`.env` is harder upfront, safer forever.

## Implementation Requirements

Every OpenClaw skill that needs credentials MUST:

1. ✅ Load from `~/.openclaw/.env`
2. ✅ Validate file permissions on load
3. ✅ Never log full credentials
4. ✅ Provide clear error if `.env` missing
5. ❌ Never accept credentials from CLI args
6. ❌ Never accept credentials from environment variables set in shell
7. ❌ Never create credential files outside `.env`

## Migration Is Mandatory

If you find credentials elsewhere:
1. Stop
2. Run credential-manager skill
3. Consolidate into `.env`
4. Validate security
5. Delete old files

**Do not proceed until credentials are properly secured.**

## The Standard

This isn't just best practice — it's the OpenClaw standard.

If a skill stores credentials anywhere other than `~/.openclaw/.env`, it's **non-compliant** and should not be used.

## Audit Checklist

Before deploying OpenClaw to production:

- [ ] All credentials in `~/.openclaw/.env`
- [ ] File mode is 600
- [ ] `.env` is in `.gitignore`
- [ ] No credentials in code
- [ ] No credentials in logs
- [ ] No credentials in shell history
- [ ] Backups are encrypted (or excluded)
- [ ] Team knows not to share `.env`

If any box is unchecked, **you're not ready**.

## Exception: None

There are no exceptions to this principle.

"But what about...":
- **Docker?** Mount `.env` securely
- **CI/CD?** Use secret managers, inject at runtime
- **Multiple environments?** `.env.dev`, `.env.prod` — same pattern
- **Team collaboration?** Share `.env.example`, not `.env`
- **Quick testing?** Still use `.env`

Every scenario has a secure solution. Use it.

## Why We're Strict

AI agents are powerful. With great power comes:
1. Greater responsibility
2. Bigger attack surface
3. More tempting target

**Scattered credentials + powerful agent = disaster waiting to happen.**

## The Foundation

Think of `.env` like:
- SSL/TLS for web servers (non-negotiable)
- Encryption for databases (mandatory)
- Authentication for APIs (required)

It's not a feature. It's the foundation everything else builds on.

---

**Bottom line:** If your credentials aren't in `~/.openclaw/.env` with mode 600, stop what you're doing and fix it first. Everything else can wait.

This is the hill we die on. 📺


path: README.md
content: # Credential Manager Skill

**Status:** ✅ Production Ready  
**Category:** 🔒 Core Security Infrastructure  
**Package:** `credential-manager.skill`  
**Version:** 2.0.0

## What This Is

**MANDATORY security foundation for OpenClaw.**

Consolidate scattered API keys and credentials into a secure `.env` file. Encrypt high-value secrets with GPG. Track credential rotation. Detect hardcoded secrets in source files.

This is not optional — it's a core requirement for secure OpenClaw deployments.

## What's New in v2.0.0

- 🔐 **GPG encryption** for private keys and wallet secrets (AES256)
- 🔍 **Deep scanning** for hardcoded secrets in source files
- 🔄 **Rotation tracking** with risk-based schedules
- 🛡️ **Backup hardening** (mode 600 files, 700 directories)
- 📊 **Entropy analysis** and private key detection in validation
- 🔗 **Symlink detection** for `.env` files
- 🐛 **Fixed** validate.py quote contradiction
- 🐛 **Fixed** backup permissions (were 644, now 600)

## Why This Matters

Scattered credentials = scattered attack surface. One `.env` file with proper permissions is:
- ✅ Easier to secure (one file, one permission)
- ✅ Easier to audit (one location to check)
- ✅ Easier to rotate (update once, everywhere works)
- ✅ Harder to leak (git-ignored by default)
- ✅ Encrypted at rest (private keys in GPG)

## Quick Start

```bash
# Navigate to the skill
cd /path/to/skills/credential-manager

# Step 1: Scan (find all credential files)
./scripts/scan.py --deep

# Step 2: Consolidate into .env
./scripts/consolidate.py

# Step 3: Validate security
./scripts/validate.py --fix

# Step 4: Setup GPG and encrypt private keys
./scripts/setup-gpg.sh
./scripts/encrypt.py --keys MAIN_WALLET_PRIVATE_KEY,CUSTODY_PRIVATE_KEY

# Step 5: Initialize rotation tracking
./scripts/rotation-check.py --init

# Step 6: Cleanup old files
./scripts/cleanup.py --confirm
```

## The Consolidation Rule

**ALL credentials MUST be in `~/.openclaw/.env` ONLY.**

No workspace, no skills, no scripts directories. Root only. No exceptions.

See `CONSOLIDATION-RULE.md` and `CORE-PRINCIPLE.md` for full rationale.

## Files Included

```
credential-manager/
├── SKILL.md                         # Full documentation + migration workflow
├── README.md                        # This file
├── CHANGELOG.md                     # Version history
├── CORE-PRINCIPLE.md                # Why centralized credentials are mandatory
├── CONSOLIDATION-RULE.md            # The single source principle
├── scripts/
│   ├── scan.py                      # Scan for credential files + deep scan
│   ├── consolidate.py               # Merge into .env with secure backups
│   ├── validate.py                  # Security validation + entropy analysis
│   ├── enforce.py                   # Fail-fast enforcement + GPG decryption
│   ├── encrypt.py                   # GPG encrypt/decrypt high-value secrets
│   ├── setup-gpg.sh                 # First-time GPG configuration
│   ├── rotation-check.py            # Credential rotation tracking
│   └── cleanup.py                   # Remove scattered credential files
└── references/
    ├── security.md                  # Security best practices
    └── supported-services.md        # Known service patterns
```

## Output Structure

After full migration:

```
~/.openclaw/
├── .env                     # All credentials (mode 600, private keys = GPG:*)
├── .env.secrets.gpg         # GPG-encrypted private keys (mode 600, AES256)
├── .env.meta                # Rotation tracking metadata (mode 600)
├── .env.example             # Template (safe to share)
├── .gitignore               # Protects .env, .env.secrets.gpg, .env.meta
└── backups/                 # (mode 700)
    └── credentials-old-YYYYMMDD/  # (mode 700)
        └── *.bak            # Backup files (mode 600)
```

## GPG Encryption

Private keys and wallet secrets should never exist as plaintext on disk:

```bash
# Encrypt (moves to .env.secrets.gpg, replaces with GPG: placeholder)
./scripts/encrypt.py --keys WALLET_PRIVATE_KEY,SIGNER_PRIVATE_KEY

# List encrypted keys
./scripts/encrypt.py --list

# Decrypt back to plaintext
./scripts/encrypt.py --decrypt --keys WALLET_PRIVATE_KEY
```

Scripts access encrypted credentials transparently:
```python
from enforce import get_credential
key = get_credential('WALLET_PRIVATE_KEY')  # Auto-decrypts from GPG
```

## Rotation Tracking

```bash
# Initialize (auto-classifies risk levels)
./scripts/rotation-check.py --init

# Check status
./scripts/rotation-check.py

# Record a rotation
./scripts/rotation-check.py --rotated API_KEY
```

Risk levels: Critical (90d), Standard (180d), Low (365d).

## Deep Scanning

```bash
# Scan source files for hardcoded secrets
./scripts/scan.py --deep
```

Detects: `sk_`/`pk_` prefixes, `0x` + 64 hex (private keys), hardcoded credential assignments, mnemonic phrases. Excludes `node_modules/` and `.git/`.

## Supported Services

- **Social:** X (Twitter), Farcaster, Molten, Moltbook, Botchan/4claw
- **AI:** OpenAI, Anthropic, Google/Gemini, OpenRouter
- **Web3:** Ethereum wallets, Coinbase/CDP, Farcaster custody/signer keys
- **Dev:** GitHub, GitLab
- **Communication:** Telegram, Discord, Slack
- **And many more...** (see `references/supported-services.md`)

## Security Features

✅ **File permissions** — `.env` at mode 600 (owner only)
✅ **Directory permissions** — Backups at mode 700 (owner only)
✅ **GPG encryption** — Private keys encrypted at rest (AES256)
✅ **Git protection** — `.env`, `.env.secrets.gpg`, `.env.meta` all git-ignored
✅ **Entropy analysis** — Flags weak/placeholder values for secret fields
✅ **Private key detection** — Flags plaintext `0x` + 64 hex values
✅ **Mnemonic detection** — Flags 12/24 word seed phrases
✅ **Deep scanning** — Finds hardcoded secrets in source files
✅ **Symlink detection** — Validates symlinked `.env` targets
✅ **Rotation tracking** — Risk-based rotation schedules with warnings
✅ **Backup hardening** — All backups secured with proper permissions
✅ **Fail-fast enforcement** — Skills refuse to run if credentials insecure

## Testing

Tested on a live OpenClaw deployment:

- ✅ Scanned and found 4 credential files (1 with insecure permissions)
- ✅ Deep scan caught 2 hardcoded test keys in scripts
- ✅ Consolidated Farcaster, Moltbook, Basecred credentials into `.env`
- ✅ GPG-encrypted 5 private keys (wallet + 4 Farcaster)
- ✅ Fixed 12 backup permission issues
- ✅ Initialized rotation tracking for 57 keys
- ✅ Updated 4 scripts to use `.env` with GPG decryption
- ✅ All validation checks pass

## Installation

### From ClawHub
```bash
clawhub install credential-manager
```

### Manual
```bash
# Copy to your skills directory
cp -r credential-manager/ ~/.openclaw/skills/
# or
cp -r credential-manager/ /path/to/openclaw/skills/
```

## License

Part of the OpenClaw project.

---

**Created:** 2026-02-05  
**Updated:** 2026-02-11  
**Author:** Mr. Tee (OpenClaw Agent)  
**Tested:** ✅ Production Ready (v2.0.0)


path: references/security.md
content: # Security Best Practices

## File Permissions

### Unix/Linux/macOS
```bash
# .env should be readable/writable only by owner
chmod 600 ~/.openclaw/.env

# Verify
ls -la ~/.openclaw/.env
# Should show: -rw------- (600)
```

### Windows
```powershell
# Remove inheritance and grant only owner access
icacls .env /inheritance:r /grant:r "%USERNAME%:F"
```

## Version Control

### Always Ignore Credentials
```gitignore
# In .gitignore
.env
*.env
.env.*
!.env.example

# Credential files
*credentials*.json
*-creds.json
*.key
*.pem

# Config directories
.config/
```

### Check Before Committing
```bash
# Scan for secrets before commit
git diff --cached | grep -i "api_key\|secret\|password\|token"

# Use git-secrets or similar tools
git secrets --scan
```

## Credential Rotation

### Regular Rotation Schedule
- **Critical services:** Every 30 days
- **Standard services:** Every 90 days
- **Low-risk services:** Every 6 months

### After Rotation
1. Update `.env` with new credentials
2. Test applications
3. Revoke old credentials
4. Document rotation date

## Separation of Environments

### Use Separate Credentials
```bash
# Development
~/.openclaw/.env.development

# Production
~/.openclaw/.env.production

# Never mix environments
```

### Load Appropriate Environment
```python
import os
env = os.getenv('OPENCLAW_ENV', 'development')
env_file = f"~/.openclaw/.env.{env}"
```

## Secret Exposure Prevention

### Never Log Full Credentials
```python
# ❌ BAD
print(f"Using API key: {api_key}")

# ✅ GOOD
print(f"Using API key: {api_key[:8]}...")
```

### Sanitize Error Messages
```python
# ❌ BAD
raise Exception(f"Auth failed with key {api_key}")

# ✅ GOOD
raise Exception("Authentication failed")
```

### Avoid Shell History
```bash
# ❌ BAD
export [REDACTED_SECRET]

# ✅ GOOD - Load from file
set -a && source ~/.openclaw/.env && set +a
```

## Encrypted Storage

### For Extra Security
```bash
# Encrypt .env with GPG
gpg -c ~/.openclaw/.env

# Decrypt when needed
gpg -d ~/.openclaw/.env.gpg > /tmp/.env
```

### Use Secret Managers
- **1Password:** OpenClaw 1password skill
- **System Keyring:** gnome-keyring, macOS Keychain
- **Cloud:** AWS Secrets Manager, Google Secret Manager

## Access Control

### Minimize Access
- Only necessary processes
- Only necessary users
- Only necessary duration

### Audit Access
```bash
# Check who can read .env
getfacl ~/.openclaw/.env

# Check recent access
stat ~/.openclaw/.env
```

## Backup Security

### Encrypt Backups
```bash
# Encrypt before backup
tar czf - ~/.openclaw | gpg -c > backup.tar.gz.gpg

# Restore
gpg -d backup.tar.gz.gpg | tar xzf -
```

### Exclude from General Backups
```bash
# In backup scripts, exclude sensitive dirs
rsync -av --exclude='.openclaw/.env' ~/ /backup/
```

## Network Security

### Never Transmit Unencrypted
- ✅ HTTPS only
- ❌ Never HTTP
- ❌ Never in URL params
- ✅ Always in headers

### API Best Practices
```python
# ✅ GOOD - In headers
headers = {'Authorization': f'Bearer {token}'}
requests.get(url, headers=headers)

# ❌ BAD - In URL
requests.get(f'{url}?[REDACTED_SECRET]')
```

## Incident Response

### If Credentials Leaked

1. **Immediately:** Revoke leaked credentials
2. **Rotate:** Generate new credentials
3. **Audit:** Check for unauthorized access
4. **Update:** Replace in `.env` and all systems
5. **Monitor:** Watch for abuse

### Example
```bash
# 1. Revoke old key in service dashboard
# 2. Generate new key
# 3. Update .env
sed -i 's/old_key/new_key/' ~/.openclaw/.env
# 4. Test
./test_credentials.sh
# 5. Monitor logs
```

## Compliance

### Know Your Requirements
- **GDPR:** Data protection regulations
- **HIPAA:** Healthcare data
- **PCI DSS:** Payment card data
- **SOC 2:** Security controls

### Audit Trail
```bash
# Log credential access
echo "$(date): .env accessed by $USER" >> ~/.openclaw/access.log
```

## Checklist

Before going to production:

- [ ] `.env` has 600 permissions
- [ ] `.env` is in `.gitignore`
- [ ] No credentials in code
- [ ] No credentials in logs
- [ ] Separate dev/prod credentials
- [ ] Rotation schedule established
- [ ] Backups are encrypted
- [ ] Access is minimized
- [ ] Incident response plan exists
- [ ] Team trained on security

## Resources

- [OWASP: API Security](https://owasp.org/www-project-api-security/)
- [12-Factor App: Config](https://12factor.net/config)
- [GitHub: Removing Sensitive Data](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository)


path: references/supported-services.md
content: # Supported Services

List of services with known credential patterns that the skill can auto-detect and normalize.

## Social & Agent Networks

### X (Twitter)
**File patterns:** `x/credentials.json`, `twitter/credentials.json`

**Keys:**
```env
X_CONSUMER_KEY=...
X_CONSUMER_SECRET=...
X_ACCESS_TOKEN=...
X_ACCESS_TOKEN_SECRET=...
X_BEARER_TOKEN=...
X_USERNAME=...
X_USER_ID=...
```

### Molten
**File patterns:** `molten-creds.json`, `molten/credentials.json`

**Keys:**
```env
MOLTEN_API_KEY=molten_...
MOLTEN_AGENT_NAME=...
MOLTEN_AGENT_ID=...
```

### Moltbook
**File patterns:** `moltbook-creds.json`, `moltbook/credentials.json`

**Keys:**
```env
MOLTBOOK_API_KEY=moltbook_sk_...
MOLTBOOK_AGENT_NAME=...
MOLTBOOK_PROFILE_URL=...
```

### Botchan / 4claw
**File patterns:** `4claw/credentials.json`, `botchan/credentials.json`

**Keys:**
```env
BOTCHAN_API_KEY=clawchan_...
BOTCHAN_AGENT_NAME=...
```

## AI Providers

### OpenAI
**Keys:**
```env
OPENAI_API_KEY=sk-...
OPENAI_ORG_ID=org-...
```

### Anthropic
**Keys:**
```env
ANTHROPIC_API_KEY=sk-ant-...
```

### Google / Gemini
**Keys:**
```env
GOOGLE_API_KEY=...
GOOGLE_APPLICATION_CREDENTIALS=path/to/service-account.json
GEMINI_API_KEY=...
```

### OpenRouter
**Keys:**
```env
OPENROUTER_API_KEY=sk-or-...
```

## Development Tools

### GitHub
**Keys:**
```env
GITHUB_TOKEN=ghp_...
GITHUB_PAT=ghp_...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
```

### GitLab
**Keys:**
```env
GITLAB_TOKEN=glpat-...
GITLAB_API_KEY=...
```

## Cloud Providers

### AWS
**Keys:**
```env
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_SESSION_TOKEN=...
AWS_DEFAULT_REGION=us-east-1
```

### Google Cloud
**Keys:**
```env
GCP_PROJECT_ID=...
GCP_SERVICE_ACCOUNT_KEY=...
GOOGLE_CLOUD_PROJECT=...
```

### Azure
**Keys:**
```env
AZURE_CLIENT_ID=...
AZURE_CLIENT_SECRET=...
AZURE_TENANT_ID=...
AZURE_SUBSCRIPTION_ID=...
```

## Databases

### PostgreSQL
**Keys:**
```env
POSTGRES_HOST=...
POSTGRES_PORT=5432
POSTGRES_DB=...
POSTGRES_USER=...
POSTGRES_PASSWORD=...
DATABASE_URL=postgresql://...
```

### MongoDB
**Keys:**
```env
MONGODB_URI=mongodb://...
MONGODB_HOST=...
MONGODB_USER=...
MONGODB_PASSWORD=...
```

### Redis
**Keys:**
```env
REDIS_URL=redis://...
REDIS_HOST=...
REDIS_PORT=6379
REDIS_PASSWORD=...
```

## Communication

### Telegram
**Keys:**
```env
TELEGRAM_BOT_TOKEN=...
TELEGRAM_API_ID=...
TELEGRAM_API_HASH=...
TELEGRAM_CHAT_ID=...
```

### Discord
**Keys:**
```env
DISCORD_BOT_TOKEN=...
DISCORD_CLIENT_ID=...
DISCORD_CLIENT_SECRET=...
DISCORD_WEBHOOK_URL=...
```

### Slack
**Keys:**
```env
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
SLACK_SIGNING_SECRET=...
SLACK_WEBHOOK_URL=...
```

### WhatsApp
**Keys:**
```env
WHATSAPP_PHONE_NUMBER=...
WHATSAPP_API_KEY=...
WHATSAPP_BUSINESS_ID=...
```

## Payment Processing

### Stripe
**Keys:**
```env
STRIPE_SECRET_KEY=sk_...
STRIPE_PUBLISHABLE_KEY=pk_...
STRIPE_WEBHOOK_SECRET=whsec_...
```

### PayPal
**Keys:**
```env
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_MODE=sandbox|live
```

## Analytics

### Google Analytics
**Keys:**
```env
GA_TRACKING_ID=UA-...
GA_MEASUREMENT_ID=G-...
```

### Mixpanel
**Keys:**
```env
MIXPANEL_TOKEN=...
MIXPANEL_SECRET=...
```

## Email Services

### SendGrid
**Keys:**
```env
SENDGRID_API_KEY=SG....
SENDGRID_FROM_EMAIL=...
```

### Mailgun
**Keys:**
```env
MAILGUN_API_KEY=...
MAILGUN_DOMAIN=...
```

### SMTP (Generic)
**Keys:**
```env
SMTP_HOST=...
SMTP_PORT=587
SMTP_USER=...
SMTP_PASSWORD=...
SMTP_FROM=...
```

## Web3 / Blockchain

### Ethereum
**Keys:**
```env
ETH_PRIVATE_KEY=0x...
ETH_RPC_URL=...
INFURA_API_KEY=...
ALCHEMY_API_KEY=...
ETHERSCAN_API_KEY=...
```

### Solana
**Keys:**
```env
SOLANA_PRIVATE_KEY=...
SOLANA_RPC_URL=...
```

## Storage

### AWS S3
**Keys:**
```env
AWS_S3_BUCKET=...
AWS_S3_REGION=...
# (Uses AWS credentials above)
```

### Cloudflare R2
**Keys:**
```env
R2_ACCOUNT_ID=...
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET=...
```

### Pinata (IPFS)
**Keys:**
```env
PINATA_API_KEY=...
PINATA_SECRET_API_KEY=...
PINATA_JWT=...
```

## Search

### Algolia
**Keys:**
```env
ALGOLIA_APP_ID=...
ALGOLIA_API_KEY=...
ALGOLIA_SEARCH_KEY=...
```

### Brave Search
**Keys:**
```env
BRAVE_API_KEY=...
```

## Generic Patterns

The skill also detects generic patterns:

- `*_API_KEY`
- `*_SECRET`
- `*_TOKEN`
- `*_PASSWORD`
- `*_CLIENT_ID`
- `*_CLIENT_SECRET`
- `*_ACCESS_KEY`

These are normalized to UPPER_SNAKE_CASE and prefixed with the service name if detected.

## Adding Custom Services

To add support for a new service, edit `scripts/consolidate.py` and add to `SERVICE_MAPPINGS`:

```python
SERVICE_MAPPINGS = {
    'myservice': {
        'api_key': 'MYSERVICE_API_KEY',
        'secret': 'MYSERVICE_SECRET',
    },
    # ... existing services
}
```

Then the skill will auto-detect and normalize your service's credentials.


path: scripts/cleanup.py
content: #!/usr/bin/env python3
"""
Clean up old credential files after migration.

ENFORCES: Consolidation Rule - All credentials in ~/.openclaw/.env ONLY.
Removes scattered .env files from workspace, skills, and scripts directories.
See CONSOLIDATION-RULE.md for details.
"""

import argparse
from pathlib import Path
from typing import List

def find_old_files() -> List[Path]:
    """Find old credential files (excluding the new .env)."""
    from scan import scan_locations
    results = scan_locations()
    
    home = Path.home()
    env_file = home / '.openclaw' / '.env'
    
    old_files = []
    for result in results:
        path = Path(result['path'])
        if path != env_file and path.exists():
            old_files.append(path)
    
    return old_files

def cleanup(confirm: bool = False, keep_backups: bool = True, dry_run: bool = True):
    """Clean up old credential files."""
    old_files = find_old_files()
    
    if not old_files:
        print("✅ No old credential files to clean up")
        return {'status': 'no_files'}
    
    print(f"\n📋 Found {len(old_files)} old credential file(s):\n")
    for f in old_files:
        print(f"   • {f}")
    
    if not confirm:
        print(f"\n⚠️  DRY RUN - No files will be deleted")
        print(f"   Run with --confirm to actually delete files")
        return {'status': 'dry_run'}
    
    # Final confirmation
    print(f"\n⚠️  WARNING: This will permanently delete {len(old_files)} file(s)")
    
    if keep_backups:
        backup_dir = Path.home() / '.openclaw' / 'backups'
        if backup_dir.exists():
            backups = list(backup_dir.glob('credentials-old-*'))
            if backups:
                print(f"\n📦 Backups exist in: {backup_dir}")
                for backup in backups:
                    print(f"   • {backup.name}")
    
    response = input(f"\n   Type 'DELETE' to confirm: ")
    if response != 'DELETE':
        print("   ❌ Cancelled")
        return {'status': 'cancelled'}
    
    # Delete files
    deleted = []
    errors = []
    
    print(f"\n🗑️  Deleting files...")
    for f in old_files:
        try:
            f.unlink()
            deleted.append(f)
            print(f"   ✅ Deleted: {f}")
        except Exception as e:
            errors.append((f, str(e)))
            print(f"   ❌ Error: {f} - {e}")
    
    # Summary
    print(f"\n📊 Summary:")
    print(f"   Deleted: {len(deleted)}")
    if errors:
        print(f"   Errors: {len(errors)}")
    
    if deleted and keep_backups:
        print(f"\n💡 Backups preserved in ~/.openclaw/backups/")
    
    print(f"\n{'✅' if not errors else '⚠️'} Cleanup {'complete' if not errors else 'finished with errors'}")
    
    return {
        'status': 'success' if not errors else 'partial',
        'deleted': len(deleted),
        'errors': len(errors)
    }

def main():
    parser = argparse.ArgumentParser(
        description='Clean up old credential files',
        epilog='IMPORTANT: Make sure to test your applications with the new .env before cleanup!'
    )
    parser.add_argument('--confirm', action='store_true',
                      help='Actually delete files (default is dry run)')
    parser.add_argument('--keep-backups', action='store_true', default=True,
                      help='Keep backup directory (default: True)')
    parser.add_argument('--force', action='store_true',
                      help='Skip final confirmation (dangerous!)')
    args = parser.parse_args()
    
    if args.force and not args.confirm:
        print("❌ --force requires --confirm")
        return 1
    
    result = cleanup(args.confirm, args.keep_backups, not args.confirm)
    return 0 if result['status'] in ['success', 'no_files', 'dry_run', 'cancelled'] else 1

if __name__ == '__main__':
    exit(main())


path: scripts/consolidate.py
content: #!/usr/bin/env python3
"""
Consolidate credentials into ~/.openclaw/.env
Backs up files with secure permissions (600 files, 700 dirs).
"""

import argparse
import json
import os
import shutil
from datetime import datetime
from pathlib import Path
from typing import Dict, List

# Service-specific key mappings
SERVICE_MAPPINGS = {
    'x': {
        'consumer_key': 'X_CONSUMER_KEY',
        'consumer_secret': 'X_CONSUMER_SECRET',
        'access_token': 'X_ACCESS_TOKEN',
        'access_token_secret': 'X_ACCESS_TOKEN_SECRET',
        'bearer_token': 'X_BEARER_TOKEN',
        'username': 'X_USERNAME',
        'user_id': 'X_USER_ID',
    },
    'farcaster': {
        'custodyPrivateKey': 'FARCASTER_CUSTODY_PRIVATE_KEY',
        'signerPrivateKey': 'FARCASTER_SIGNER_PRIVATE_KEY',
        'signerPublicKey': 'FARCASTER_SIGNER_PUBLIC_KEY',
        'custodyAddress': 'FARCASTER_CUSTODY_ADDRESS',
        'fid': 'FARCASTER_FID',
        'fname': 'FARCASTER_FNAME',
    },
    'molten': {
        'api_key': 'MOLTEN_API_KEY',
        'agent_name': 'MOLTEN_AGENT_NAME',
        'agent_id': 'MOLTEN_AGENT_ID',
    },
    'moltbook': {
        'api_key': 'MOLTBOOK_API_KEY',
        'agent_name': 'MOLTBOOK_AGENT_NAME',
        'profile_url': 'MOLTBOOK_PROFILE_URL',
    },
    'botchan': {
        'api_key': 'BOTCHAN_API_KEY',
        'agent_name': 'BOTCHAN_AGENT_NAME',
    },
    '4claw': {
        'api_key': 'BOTCHAN_API_KEY',
        'name': 'BOTCHAN_AGENT_NAME',
    },
}


def detect_service(path: Path, data: Dict) -> str:
    """Detect service from path or data keys."""
    path_str = str(path).lower()

    # Check path
    for service in SERVICE_MAPPINGS:
        if service in path_str:
            return service

    # Check data keys
    keys = set(k.lower() for k in data.keys())
    if 'consumer_key' in keys or 'consumer_secret' in keys:
        return 'x'
    if 'custodyprivatekey' in keys or 'signerprivatekey' in keys:
        return 'farcaster'
    if 'agent_id' in keys and 'api_key' in keys:
        if 'molten' in str(data.get('api_key', '')):
            return 'molten'
        if 'moltbook' in str(data.get('api_key', '')):
            return 'moltbook'

    return 'generic'


def normalize_key(key: str, service: str) -> str:
    """Normalize a key to ENV format."""
    if service in SERVICE_MAPPINGS:
        mapping = SERVICE_MAPPINGS[service]
        if key in mapping:
            return mapping[key]

    # Generic normalization
    env_key = key.upper()
    env_key = env_key.replace('-', '_')
    env_key = env_key.replace(' ', '_')

    # Add service prefix if not generic
    if service != 'generic' and not env_key.startswith(service.upper()):
        env_key = f"{service.upper()}_{env_key}"

    return env_key


def backup_files(files: List[Path], backup_dir: Path):
    """Backup files before modification with secure permissions."""
    backup_dir.mkdir(parents=True, exist_ok=True)
    os.chmod(backup_dir, 0o700)

    # Also secure parent backup dir
    parent = backup_dir.parent
    if parent.name == 'backups':
        os.chmod(parent, 0o700)

    for file in files:
        if file.exists():
            backup_name = file.name + '.bak'
            if '/' in str(file):
                rel_parts = file.relative_to(Path.home()).parts
                backup_name = '-'.join(rel_parts) + '.bak'

            backup_path = backup_dir / backup_name
            shutil.copy2(file, backup_path)
            os.chmod(backup_path, 0o600)
            print(f"   📦 Backed up: {file} → {backup_path}")


def load_credentials(path: Path) -> Dict:
    """Load credentials from a file."""
    if path.suffix == '.json':
        with open(path) as f:
            data = json.load(f)
        # Flatten nested dicts for .env compatibility
        flat = {}
        for k, v in data.items() if isinstance(data, dict) else []:
            if isinstance(v, (dict, list)):
                flat[k] = json.dumps(v, separators=(',', ':'))
            else:
                flat[k] = str(v)
        return flat if flat else (data if isinstance(data, dict) else {})
    elif '.env' in path.name:
        creds = {}
        with open(path) as f:
            for line in f:
                line = line.strip()
                if '=' in line and not line.startswith('#'):
                    key, val = line.split('=', 1)
                    creds[key.strip()] = val.strip()
        return creds
    return {}


def consolidate(service_filter: str = None, backup_only: bool = False,
                auto_yes: bool = False) -> Dict:
    """Consolidate credentials into .env"""
    home = Path.home()
    openclaw_dir = home / '.openclaw'
    env_file = openclaw_dir / '.env'
    env_example = openclaw_dir / '.env.example'
    backup_dir = openclaw_dir / 'backups' / f'credentials-old-{datetime.now().strftime("%Y%m%d")}'

    # Scan for files
    from scan import scan_locations
    results = scan_locations()

    if not results:
        print("✅ No credential files found to migrate")
        return {'status': 'no_files'}

    print(f"\n📋 Found {len(results)} credential file(s) to migrate\n")

    # Backup existing .env
    if env_file.exists():
        backup_files([env_file], backup_dir)

    # Load existing .env if present
    env_data = {}
    if env_file.exists():
        env_data = load_credentials(env_file)
        print(f"   📝 Loading existing .env ({len(env_data)} keys)")

    # Process each file
    files_to_backup = []
    new_keys = {}

    for result in results:
        path = Path(result['path'])
        if not path.exists() or path == env_file:
            continue

        # Skip symlinks that point to main .env
        if path.is_symlink() and path.resolve() == env_file:
            continue

        print(f"\n🔍 Processing: {path}")

        try:
            data = load_credentials(path)
            service = detect_service(path, data)

            if service_filter and service != service_filter:
                print(f"   ⏭️  Skipping (service filter)")
                continue

            print(f"   🏷️  Detected service: {service}")

            for key, value in data.items():
                env_key = normalize_key(key, service)
                if env_key not in env_data:
                    new_keys[env_key] = value
                    print(f"      {key} → {env_key}")

            files_to_backup.append(path)

        except Exception as e:
            print(f"   ❌ Error: {e}")

    if not new_keys and not backup_only:
        print("\n✅ No new credentials to add")
        return {'status': 'no_new_keys'}

    # Confirm
    if not auto_yes and not backup_only:
        print(f"\n📊 Summary:")
        print(f"   New keys to add: {len(new_keys)}")
        print(f"   Files to backup: {len(files_to_backup)}")
        response = input("\n   Proceed? [y/N] ")
        if response.lower() != 'y':
            print("   ❌ Cancelled")
            return {'status': 'cancelled'}

    # Backup files
    if files_to_backup:
        print(f"\n📦 Backing up {len(files_to_backup)} file(s)...")
        backup_files(files_to_backup, backup_dir)

    if backup_only:
        print(f"\n✅ Backup complete: {backup_dir}")
        return {'status': 'backup_only', 'backup_dir': str(backup_dir)}

    # Write .env
    print(f"\n✍️  Writing .env...")
    openclaw_dir.mkdir(parents=True, exist_ok=True)

    # Merge and write
    env_data.update(new_keys)

    with open(env_file, 'w') as f:
        f.write("# OpenClaw Agent Credentials\n")
        f.write("# Generated by credential-manager skill\n\n")

        # Group by service
        services = {}
        for key, value in sorted(env_data.items()):
            service = key.split('_')[0] if '_' in key else 'OTHER'
            if service not in services:
                services[service] = []
            services[service].append((key, value))

        for service, items in sorted(services.items()):
            f.write(f"# {service}\n")
            for key, value in items:
                f.write(f"{key}={value}\n")
            f.write("\n")

    # Set permissions
    os.chmod(env_file, 0o600)
    print(f"   🔒 Set permissions: 600")

    # Create .env.example
    with open(env_example, 'w') as f:
        f.write("# OpenClaw Agent Credentials Template\n")
        f.write("# Copy to .env and fill in your actual values\n\n")
        for key in sorted(env_data.keys()):
            f.write(f"{key}=your_value_here\n")

    print(f"   📄 Created .env.example")

    # Update .gitignore
    gitignore = openclaw_dir / '.gitignore'
    gitignore_entries = ['.env', '.env.secrets.gpg', '.env.meta']
    if gitignore.exists():
        content = gitignore.read_text()
    else:
        content = ''

    missing = [e for e in gitignore_entries if e not in content]
    if missing:
        with open(gitignore, 'a') as f:
            f.write("\n# Credentials\n")
            for entry in missing:
                f.write(f"{entry}\n")
        print(f"   🚫 Updated .gitignore")

    print(f"\n✅ Migration complete!")
    print(f"   📁 Credentials: {env_file}")
    print(f"   📦 Backups: {backup_dir}")
    print(f"\n💡 Next: Run ./scripts/validate.py to verify security")

    return {
        'status': 'success',
        'env_file': str(env_file),
        'backup_dir': str(backup_dir),
        'keys_added': len(new_keys),
    }


def main():
    parser = argparse.ArgumentParser(description='Consolidate credentials')
    parser.add_argument('--service', help='Filter by service')
    parser.add_argument('--backup-only', action='store_true',
                        help='Backup files without consolidating')
    parser.add_argument('--yes', '-y', action='store_true',
                        help='Auto-confirm all prompts')
    args = parser.parse_args()

    result = consolidate(args.service, args.backup_only, args.yes)
    return 0 if result['status'] in ['success', 'no_files', 'backup_only'] else 1


if __name__ == '__main__':
    exit(main())


path: scripts/encrypt.py
content: #!/usr/bin/env python3
"""
GPG encryption for high-value secrets.
Encrypts specified keys from .env into .env.secrets.gpg
"""

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path


def is_gpg_available() -> bool:
    """Check if GPG is available."""
    try:
        subprocess.run(['gpg', '--version'], capture_output=True, check=True)
        return True
    except (FileNotFoundError, subprocess.CalledProcessError):
        return False


def load_env(env_file: Path) -> dict:
    """Load .env file as dict preserving order."""
    data = {}
    with open(env_file) as f:
        for line in f:
            line = line.strip()
            if '=' in line and not line.startswith('#'):
                k, v = line.split('=', 1)
                data[k.strip()] = v.strip()
    return data


def read_env_lines(env_file: Path) -> list:
    """Read .env file preserving all lines (comments, blanks)."""
    with open(env_file) as f:
        return f.readlines()


def write_env_lines(env_file: Path, lines: list):
    """Write lines back to .env with secure permissions."""
    with open(env_file, 'w') as f:
        f.writelines(lines)
    os.chmod(env_file, 0o600)


def load_secrets(secrets_file: Path, passphrase: str = None) -> dict:
    """Decrypt and load existing secrets from GPG file."""
    if not secrets_file.exists():
        return {}

    if passphrase is None:
        passphrase = _get_passphrase()

    try:
        result = subprocess.run(
            ['gpg', '-d', '--batch', '--quiet',
             '--passphrase-fd', '0',
             str(secrets_file)],
            input=passphrase,
            capture_output=True, text=True, timeout=30
        )
        if result.returncode == 0:
            return json.loads(result.stdout)
        else:
            print(f"❌ GPG decryption failed: {result.stderr.strip()}", file=sys.stderr)
    except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
        print(f"❌ Failed to load secrets: {e}", file=sys.stderr)

    return {}


def _get_passphrase() -> str:
    """Get passphrase from environment or prompt."""
    # Check environment variable first (for automation)
    passphrase = os.environ.get('OPENCLAW_GPG_PASSPHRASE')
    if passphrase:
        return passphrase

    # Interactive prompt
    import getpass
    passphrase = getpass.getpass("🔑 Enter GPG passphrase for OpenClaw secrets: ")
    if not passphrase:
        print("❌ Passphrase cannot be empty", file=sys.stderr)
        sys.exit(1)
    return passphrase


def save_secrets(secrets_file: Path, secrets: dict, passphrase: str = None):
    """Encrypt and save secrets to GPG file."""
    json_data = json.dumps(secrets, indent=2)

    if passphrase is None:
        passphrase = _get_passphrase()

    # Write to temp file first, then encrypt
    tmp_file = secrets_file.parent / '.env.secrets.tmp'
    try:
        with open(tmp_file, 'w') as f:
            f.write(json_data)
        os.chmod(tmp_file, 0o600)

        # Encrypt with symmetric cipher using passphrase via fd
        result = subprocess.run(
            ['gpg', '-c', '--batch', '--yes',
             '--cipher-algo', 'AES256',
             '--passphrase-fd', '0',
             '-o', str(secrets_file),
             str(tmp_file)],
            input=passphrase,
            capture_output=True, text=True, timeout=30
        )

        if result.returncode != 0:
            print(f"❌ GPG encryption failed: {result.stderr.strip()}", file=sys.stderr)
            return False

        os.chmod(secrets_file, 0o600)
        return True

    finally:
        # Always clean up temp file
        if tmp_file.exists():
            tmp_file.unlink()


def encrypt_keys(key_names: list):
    """Encrypt specified keys from .env into GPG secrets file."""
    home = Path.home()
    env_file = home / '.openclaw' / '.env'
    secrets_file = home / '.openclaw' / '.env.secrets.gpg'

    if not env_file.exists():
        print("❌ ~/.openclaw/.env does not exist")
        return False

    if not is_gpg_available():
        print("❌ GPG is not installed")
        print("   Install: sudo apt install gnupg")
        print("   Then run: ./scripts/setup-gpg.sh")
        return False

    # Get passphrase once for the whole operation
    passphrase = _get_passphrase()

    # Load current .env
    env_data = load_env(env_file)
    env_lines = read_env_lines(env_file)

    # Load existing secrets
    secrets = load_secrets(secrets_file, passphrase)

    encrypted = []
    not_found = []

    for key in key_names:
        key = key.strip()
        if key not in env_data:
            not_found.append(key)
            continue

        value = env_data[key]

        # Skip if already encrypted
        if value.startswith('GPG:'):
            print(f"   ⏭️  {key}: already encrypted")
            continue

        # Move value to secrets
        secrets[key] = value
        encrypted.append(key)

        # Replace in .env lines with GPG placeholder
        for i, line in enumerate(env_lines):
            stripped = line.strip()
            if stripped.startswith(f'{key}=') and not stripped.startswith('#'):
                env_lines[i] = f"{key}=GPG:{key}\n"
                break

    if not_found:
        print(f"\n⚠️  Keys not found in .env: {', '.join(not_found)}")

    if not encrypted:
        print("\n✅ No new keys to encrypt")
        return True

    # Save encrypted secrets
    print(f"\n🔐 Encrypting {len(encrypted)} key(s)...")
    if not save_secrets(secrets_file, secrets, passphrase):
        return False

    # Update .env with GPG placeholders
    write_env_lines(env_file, env_lines)

    print(f"   ✅ Encrypted: {', '.join(encrypted)}")
    print(f"   📁 Secrets: {secrets_file}")
    print(f"   📝 .env updated with GPG: placeholders")

    return True


def decrypt_keys(key_names: list):
    """Decrypt specified keys back from GPG to plaintext .env."""
    home = Path.home()
    env_file = home / '.openclaw' / '.env'
    secrets_file = home / '.openclaw' / '.env.secrets.gpg'

    if not secrets_file.exists():
        print("❌ No GPG secrets file found")
        return False

    # Get passphrase once
    passphrase = _get_passphrase()

    env_data = load_env(env_file)
    env_lines = read_env_lines(env_file)
    secrets = load_secrets(secrets_file, passphrase)

    decrypted = []

    for key in key_names:
        key = key.strip()
        value = env_data.get(key, '')

        if not value.startswith('GPG:'):
            print(f"   ⏭️  {key}: not GPG-encrypted")
            continue

        if key not in secrets:
            print(f"   ⚠️  {key}: not found in GPG secrets")
            continue

        # Restore value in .env
        for i, line in enumerate(env_lines):
            stripped = line.strip()
            if stripped.startswith(f'{key}=') and not stripped.startswith('#'):
                env_lines[i] = f"{key}={secrets[key]}\n"
                break

        # Remove from secrets
        del secrets[key]
        decrypted.append(key)

    if not decrypted:
        print("\n✅ No keys to decrypt")
        return True

    # Update secrets file (or remove if empty)
    if secrets:
        save_secrets(secrets_file, secrets, passphrase)
    else:
        secrets_file.unlink()
        print("   🗑️  Removed empty secrets file")

    # Update .env
    write_env_lines(env_file, env_lines)

    print(f"\n🔓 Decrypted {len(decrypted)} key(s): {', '.join(decrypted)}")

    return True


def list_encrypted():
    """List currently encrypted keys."""
    home = Path.home()
    env_file = home / '.openclaw' / '.env'

    if not env_file.exists():
        print("❌ ~/.openclaw/.env does not exist")
        return

    env_data = load_env(env_file)
    gpg_keys = [k for k, v in env_data.items() if v.startswith('GPG:')]

    if gpg_keys:
        print(f"\n🔐 GPG-encrypted keys ({len(gpg_keys)}):\n")
        for key in sorted(gpg_keys):
            print(f"   • {key}")

        secrets_file = home / '.openclaw' / '.env.secrets.gpg'
        if secrets_file.exists():
            print(f"\n   📁 Secrets file: {secrets_file}")
            mode = oct(secrets_file.stat().st_mode)[-3:]
            status = "✅" if mode == '600' else f"⚠️  mode {mode}"
            print(f"   🔒 Permissions: {status}")
        else:
            print(f"\n   ⚠️  Secrets file missing!")
    else:
        print("\n📋 No GPG-encrypted keys found in .env")


def main():
    parser = argparse.ArgumentParser(description='GPG encryption for high-value secrets')
    parser.add_argument('--keys', help='Comma-separated list of keys to encrypt/decrypt')
    parser.add_argument('--list', action='store_true', help='List encrypted keys')
    parser.add_argument('--decrypt', action='store_true', help='Decrypt keys back to plaintext')
    args = parser.parse_args()

    if args.list:
        list_encrypted()
        return 0

    if not args.keys:
        parser.print_help()
        print("\n💡 Examples:")
        print("   ./scripts/encrypt.py --keys MAIN_WALLET_PRIVATE_KEY,FARCASTER_CUSTODY_PRIVATE_KEY")
        print("   ./scripts/encrypt.py --list")
        print("   ./scripts/encrypt.py --decrypt --keys MAIN_WALLET_PRIVATE_KEY")
        return 1

    key_list = [k.strip() for k in args.keys.split(',')]

    if args.decrypt:
        success = decrypt_keys(key_list)
    else:
        success = encrypt_keys(key_list)

    return 0 if success else 1


if __name__ == '__main__':
    exit(main())


path: scripts/enforce.py
content: #!/usr/bin/env python3
"""
Enforce .env requirement for OpenClaw skills.
Supports GPG-encrypted credentials transparently.

Usage: Import this in your skill's scripts to validate credentials are properly secured.

Example:
    from enforce import require_secure_env, get_credential

    require_secure_env()

    # Works for both plaintext and GPG-encrypted keys
    [REDACTED_SECRET]'SERVICE_API_KEY')
    wallet_key = get_credential('MAIN_WALLET_PRIVATE_KEY')  # Auto-decrypts from GPG
"""

import json
import os
import subprocess
import sys
from pathlib import Path


def check_env_exists() -> bool:
    """Check if .env file exists."""
    env_file = Path.home() / '.openclaw' / '.env'
    return env_file.exists()


def check_env_permissions() -> bool:
    """Check if .env has correct permissions (600)."""
    env_file = Path.home() / '.openclaw' / '.env'
    if not env_file.exists():
        return False
    mode = oct(env_file.stat().st_mode)[-3:]
    return mode == '600'


def check_gitignore() -> bool:
    """Check if .env is git-ignored."""
    gitignore = Path.home() / '.openclaw' / '.gitignore'
    if not gitignore.exists():
        return False
    return '.env' in gitignore.read_text()


def is_gpg_available() -> bool:
    """Check if GPG is available on the system."""
    try:
        subprocess.run(['gpg', '--version'], capture_output=True, check=True)
        return True
    except (FileNotFoundError, subprocess.CalledProcessError):
        return False


def _load_env_file() -> dict:
    """Load all key-value pairs from .env file."""
    env_file = Path.home() / '.openclaw' / '.env'
    data = {}
    with open(env_file) as f:
        for line in f:
            line = line.strip()
            if '=' in line and not line.startswith('#'):
                k, v = line.split('=', 1)
                data[k.strip()] = v.strip()
    return data


def _decrypt_gpg_key(key_name: str) -> str:
    """Decrypt a specific key from the GPG-encrypted secrets file."""
    secrets_file = Path.home() / '.openclaw' / '.env.secrets.gpg'

    if not secrets_file.exists():
        print(f"\n❌ GPG secrets file not found: {secrets_file}", file=sys.stderr)
        print("Run: ./scripts/encrypt.py --keys ... to set up GPG encryption\n", file=sys.stderr)
        return None

    if not is_gpg_available():
        print("\n❌ GPG is not installed. Cannot decrypt secrets.", file=sys.stderr)
        print("Install: sudo apt install gnupg\n", file=sys.stderr)
        return None

    try:
        result = subprocess.run(
            ['gpg', '-d', '--batch', '--quiet', str(secrets_file)],
            capture_output=True, text=True, timeout=30
        )
        if result.returncode != 0:
            print(f"\n❌ GPG decryption failed: {result.stderr.strip()}", file=sys.stderr)
            return None

        secrets = json.loads(result.stdout)
        return secrets.get(key_name)

    except subprocess.TimeoutExpired:
        print("\n❌ GPG decryption timed out (passphrase may be needed)", file=sys.stderr)
        return None
    except json.JSONDecodeError:
        print("\n❌ GPG secrets file is corrupted", file=sys.stderr)
        return None


def require_secure_env(exit_on_fail: bool = True) -> bool:
    """
    Enforce secure .env setup.

    Args:
        exit_on_fail: If True, exit with error. If False, return bool.

    Returns:
        True if all checks pass, False otherwise.
    """
    checks = [
        (check_env_exists, "❌ ~/.openclaw/.env does not exist"),
        (check_env_permissions, "❌ ~/.openclaw/.env has insecure permissions (should be 600)"),
        (check_gitignore, "❌ .env is not git-ignored"),
    ]

    failed = []
    for check_fn, error_msg in checks:
        if not check_fn():
            failed.append(error_msg)

    if failed:
        print("\n🔒 SECURITY REQUIREMENT NOT MET\n", file=sys.stderr)
        print("OpenClaw requires centralized credential management.", file=sys.stderr)
        print("\nIssues found:", file=sys.stderr)
        for msg in failed:
            print(f"  {msg}", file=sys.stderr)

        print("\n💡 Fix this by running:", file=sys.stderr)
        print("   cd ~/.openclaw/skills/credential-manager", file=sys.stderr)
        print("   ./scripts/consolidate.py", file=sys.stderr)
        print("   ./scripts/validate.py --fix", file=sys.stderr)
        print("\nSee CORE-PRINCIPLE.md for why this is mandatory.\n", file=sys.stderr)

        if exit_on_fail:
            sys.exit(1)
        return False

    return True


def get_credential(key: str, required: bool = True) -> str:
    """
    Safely get a credential from .env, with GPG decryption support.

    If the value starts with 'GPG:', the actual value is decrypted from
    ~/.openclaw/.env.secrets.gpg transparently.

    Args:
        key: Credential key (e.g., 'X_ACCESS_TOKEN')
        required: If True, exit on missing key. If False, return None.

    Returns:
        Credential value (or None if not required and missing)

    Raises:
        SystemExit: If .env not secure or key not found (when required=True)
    """
    require_secure_env()

    env_data = _load_env_file()
    value = env_data.get(key)

    if value is None:
        if not required:
            return None
        print(f"\n❌ Credential '{key}' not found in .env\n", file=sys.stderr)
        print("Add it to ~/.openclaw/.env:", file=sys.stderr)
        print(f"   {key}=your_value_here\n", file=sys.stderr)
        sys.exit(1)

    # Handle GPG-encrypted values
    if value.startswith('GPG:'):
        gpg_key = value[4:]  # Strip 'GPG:' prefix
        decrypted = _decrypt_gpg_key(gpg_key)
        if decrypted is None:
            if not required:
                return None
            print(f"\n❌ Failed to decrypt '{key}' from GPG secrets\n", file=sys.stderr)
            sys.exit(1)
        return decrypted

    return value


if __name__ == '__main__':
    # When run directly, validate and report
    print("🔍 Checking OpenClaw credential security...\n")

    if require_secure_env(exit_on_fail=False):
        print("✅ All security checks passed")
        print("\nYour credentials are properly secured:")
        print("  • ~/.openclaw/.env exists")
        print("  • Permissions are 600 (owner only)")
        print("  • Git-ignored")

        # Check GPG status
        if is_gpg_available():
            secrets_file = Path.home() / '.openclaw' / '.env.secrets.gpg'
            if secrets_file.exists():
                print("  • GPG encryption active ✅")
            else:
                print("  • GPG available but no encrypted secrets")
        else:
            print("  • GPG not installed (optional)")

        print("\n🔒 Good job! Your OpenClaw deployment follows security best practices.")
        sys.exit(0)
    else:
        sys.exit(1)


path: scripts/rotation-check.py
content: #!/usr/bin/env python3
"""
Credential rotation tracking.
Tracks creation dates, rotation schedules, and warns when keys need rotation.
"""

import argparse
import json
import os
import re
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict


# Risk classification patterns
RISK_PATTERNS = {
    'critical': [
        r'PRIVATE_KEY', r'MNEMONIC', r'SEED', r'WALLET_KEY',
        r'CUSTODY', r'SIGNER', r'PASSPHRASE',
    ],
    'standard': [
        r'API_KEY', r'SECRET', r'TOKEN', r'BEARER',
        r'CONSUMER', r'ACCESS', r'AUTH',
    ],
    'low': [
        r'.*',  # Everything else
    ],
}

ROTATION_DAYS = {
    'critical': 90,
    'standard': 180,
    'low': 365,
}


def classify_risk(key_name: str) -> str:
    """Classify a key's risk level based on its name."""
    key_upper = key_name.upper()
    for level in ['critical', 'standard']:
        for pattern in RISK_PATTERNS[level]:
            if re.search(pattern, key_upper):
                return level
    return 'low'


def load_env_keys(env_file: Path) -> list:
    """Load key names from .env file."""
    keys = []
    with open(env_file) as f:
        for line in f:
            line = line.strip()
            if '=' in line and not line.startswith('#'):
                key = line.split('=', 1)[0].strip()
                keys.append(key)
    return keys


def load_meta(meta_file: Path) -> dict:
    """Load rotation metadata."""
    if not meta_file.exists():
        return {}
    with open(meta_file) as f:
        return json.load(f)


def save_meta(meta_file: Path, meta: dict):
    """Save rotation metadata with secure permissions."""
    with open(meta_file, 'w') as f:
        json.dump(meta, f, indent=2, default=str)
    os.chmod(meta_file, 0o600)


def init_meta(env_file: Path, meta_file: Path):
    """Initialize rotation metadata for all keys in .env."""
    keys = load_env_keys(env_file)
    existing = load_meta(meta_file)
    today = datetime.now().strftime('%Y-%m-%d')

    meta = {}
    new_count = 0

    for key in keys:
        if key in existing:
            meta[key] = existing[key]
        else:
            risk = classify_risk(key)
            meta[key] = {
                'created': today,
                'lastRotated': None,
                'rotationDays': ROTATION_DAYS[risk],
                'risk': risk,
            }
            new_count += 1

    save_meta(meta_file, meta)

    print(f"\n📋 Initialized rotation tracking")
    print(f"   Total keys: {len(meta)}")
    print(f"   New entries: {new_count}")
    print(f"   Critical: {sum(1 for v in meta.values() if v['risk'] == 'critical')}")
    print(f"   Standard: {sum(1 for v in meta.values() if v['risk'] == 'standard')}")
    print(f"   Low: {sum(1 for v in meta.values() if v['risk'] == 'low')}")
    print(f"\n   📁 Metadata: {meta_file}")


def record_rotation(meta_file: Path, key_name: str):
    """Record that a key was rotated today."""
    meta = load_meta(meta_file)

    if key_name not in meta:
        print(f"⚠️  Key '{key_name}' not found in rotation metadata")
        print(f"   Run: ./scripts/rotation-check.py --init")
        return

    meta[key_name]['lastRotated'] = datetime.now().strftime('%Y-%m-%d')
    save_meta(meta_file, meta)
    print(f"✅ Recorded rotation for {key_name} ({datetime.now().strftime('%Y-%m-%d')})")


def check_rotation(meta_file: Path) -> bool:
    """Check rotation status of all keys."""
    meta = load_meta(meta_file)

    if not meta:
        print("\n⚠️  No rotation metadata found")
        print("   Run: ./scripts/rotation-check.py --init")
        return False

    today = datetime.now()
    overdue = []
    upcoming = []
    ok = []

    print(f"\n🔄 Credential Rotation Status ({today.strftime('%Y-%m-%d')})\n")

    for key, info in sorted(meta.items(), key=lambda x: x[1]['risk']):
        risk = info['risk']
        rotation_days = info['rotationDays']
        last_date_str = info.get('lastRotated') or info.get('created', today.strftime('%Y-%m-%d'))

        try:
            last_date = datetime.strptime(last_date_str, '%Y-%m-%d')
        except (ValueError, TypeError):
            last_date = today

        age_days = (today - last_date).days
        next_rotation = last_date + timedelta(days=rotation_days)
        days_until = (next_rotation - today).days

        if days_until < 0:
            overdue.append((key, risk, age_days, rotation_days, abs(days_until)))
        elif days_until < 14:
            upcoming.append((key, risk, age_days, rotation_days, days_until))
        else:
            ok.append((key, risk, age_days, rotation_days, days_until))

    # Print overdue
    if overdue:
        print("🔴 OVERDUE:")
        for key, risk, age, rot, overdue_days in overdue:
            print(f"   ❌ {key}")
            print(f"      Risk: {risk} | Age: {age}d | Rotate every: {rot}d | Overdue: {overdue_days}d")
        print()

    # Print upcoming
    if upcoming:
        print("🟡 UPCOMING (within 14 days):")
        for key, risk, age, rot, days_left in upcoming:
            print(f"   ⚠️  {key}")
            print(f"      Risk: {risk} | Age: {age}d | Rotate every: {rot}d | Due in: {days_left}d")
        print()

    # Print OK summary
    if ok:
        print(f"✅ OK: {len(ok)} key(s) within rotation schedule")
        # Show critical/standard detail even if OK
        for key, risk, age, rot, days_left in ok:
            if risk in ('critical', 'standard'):
                print(f"   ✅ {key} ({risk}, {age}d old, due in {days_left}d)")
        print()

    # Summary
    total = len(overdue) + len(upcoming) + len(ok)
    print(f"📊 Summary: {total} keys tracked")
    print(f"   🔴 Overdue: {len(overdue)}")
    print(f"   🟡 Upcoming: {len(upcoming)}")
    print(f"   ✅ OK: {len(ok)}")

    if overdue:
        print(f"\n💡 Rotate overdue keys and record:")
        print(f"   ./scripts/rotation-check.py --rotated KEY_NAME")

    return len(overdue) == 0


def main():
    parser = argparse.ArgumentParser(description='Credential rotation tracking')
    parser.add_argument('--init', action='store_true',
                        help='Initialize rotation metadata for all keys')
    parser.add_argument('--rotated', metavar='KEY',
                        help='Record that a key was rotated today')
    parser.add_argument('--meta-file', default=None,
                        help='Path to metadata file (default: ~/.openclaw/.env.meta)')
    args = parser.parse_args()

    home = Path.home()
    env_file = home / '.openclaw' / '.env'
    meta_file = Path(args.meta_file) if args.meta_file else home / '.openclaw' / '.env.meta'

    if not env_file.exists():
        print("❌ ~/.openclaw/.env does not exist")
        return 1

    if args.init:
        init_meta(env_file, meta_file)
        return 0

    if args.rotated:
        record_rotation(meta_file, args.rotated)
        return 0

    # Default: check rotation status
    all_ok = check_rotation(meta_file)
    return 0 if all_ok else 1


if __name__ == '__main__':
    exit(main())


path: scripts/scan.py
content: #!/usr/bin/env python3
"""
Scan for credential files in common locations.
Supports deep scanning for hardcoded secrets in source files.
"""

import argparse
import json
import math
import os
import re
from pathlib import Path
from typing import Dict, List

# Common credential file patterns
CREDENTIAL_PATTERNS = [
    "~/.config/*/credentials.json",
    "~/.config/*/*.credentials.json",
    "~/.openclaw/*.json",
    "~/.openclaw/*-credentials*",
    "~/.openclaw/workspace/memory/*-creds.json",
    "~/.openclaw/workspace/memory/*credentials*.json",
    "~/.openclaw/workspace/.env",
    "~/.openclaw/workspace/.env.*",
    "~/.openclaw/workspace/*/.env",
    "~/.openclaw/workspace/skills/*/.env",
    "~/.openclaw/workspace/skills/*/repo/.env",
    "~/.openclaw/workspace/scripts/.env",
    "~/.local/share/*/credentials.json",
]

# Sensitive key patterns
SENSITIVE_KEYS = [
    r"api[_-]?key",
    r"access[_-]?token",
    r"secret",
    r"password",
    r"passphrase",
    r"credentials",
    r"auth",
    r"bearer",
    r"oauth",
    r"consumer[_-]?key",
    r"private[_-]?key",
    r"mnemonic",
    r"seed[_-]?phrase",
    r"signing[_-]?key",
    r"wallet[_-]?key",
]

# Deep scan patterns for hardcoded secrets
DEEP_SCAN_PATTERNS = [
    (r'(?:sk_|pk_|Bearer\s+)[A-Za-z0-9_\-]{20,}', 'API key prefix (sk_/pk_/Bearer)'),
    (r'0x[a-fA-F0-9]{64}', 'Possible private key (0x + 64 hex)'),
    (r'(?:api[_-]?key|secret|token|password)\s*[=:]\s*["\']?[A-Za-z0-9_\-]{16,}', 'Hardcoded credential assignment'),
    (r'(?:mnemonic|seed)\s*[=:]\s*["\'](?:\w+\s+){11,23}\w+', 'Possible mnemonic/seed phrase'),
]

DEEP_SCAN_EXTENSIONS = {'.sh', '.js', '.py', '.mjs', '.ts', '.jsx', '.tsx'}
DEEP_SCAN_EXCLUDE = {'node_modules', '.git', '__pycache__', '.venv', 'venv'}


def calc_entropy(s: str) -> float:
    """Calculate Shannon entropy of a string in bits per character."""
    if not s:
        return 0.0
    freq = {}
    for c in s:
        freq[c] = freq.get(c, 0) + 1
    length = len(s)
    return -sum((count / length) * math.log2(count / length) for count in freq.values())


def scan_json_file(path: Path) -> Dict:
    """Scan a JSON file for credentials."""
    try:
        with open(path) as f:
            data = json.load(f)

        keys = []
        if isinstance(data, dict):
            keys = [k.lower() for k in data.keys()]

        has_sensitive = any(
            any(re.search(pattern, key, re.IGNORECASE) for pattern in SENSITIVE_KEYS)
            for key in keys
        )

        result = {
            "path": str(path),
            "type": "json",
            "keys": list(data.keys()) if isinstance(data, dict) else [],
            "likely_credentials": has_sensitive,
            "size": path.stat().st_size,
            "mode": oct(path.stat().st_mode)[-3:],
        }

        # Check if symlink
        if path.is_symlink():
            result["symlink_target"] = str(path.resolve())

        return result
    except Exception as e:
        return {
            "path": str(path),
            "type": "json",
            "error": str(e),
            "likely_credentials": True,
        }


def scan_env_file(path: Path) -> Dict:
    """Scan a .env file for credentials."""
    try:
        keys = []
        with open(path) as f:
            for line in f:
                line = line.strip()
                if '=' in line and not line.startswith('#'):
                    key = line.split('=', 1)[0].strip()
                    keys.append(key)

        result = {
            "path": str(path),
            "type": "env",
            "keys": keys,
            "likely_credentials": len(keys) > 0,
            "size": path.stat().st_size,
            "mode": oct(path.stat().st_mode)[-3:],
        }

        # Check if symlink
        if path.is_symlink():
            target = path.resolve()
            result["symlink_target"] = str(target)
            main_env = Path.home() / '.openclaw' / '.env'
            result["symlink_ok"] = target == main_env

        return result
    except Exception as e:
        return {
            "path": str(path),
            "type": "env",
            "error": str(e),
            "likely_credentials": True,
        }


def deep_scan(scan_dir: Path = None) -> List[Dict]:
    """Scan source files for hardcoded credentials."""
    if scan_dir is None:
        scan_dir = Path.home() / '.openclaw' / 'workspace'

    findings = []

    for root, dirs, files in os.walk(scan_dir):
        # Prune excluded directories
        dirs[:] = [d for d in dirs if d not in DEEP_SCAN_EXCLUDE]

        for fname in files:
            fpath = Path(root) / fname
            if fpath.suffix not in DEEP_SCAN_EXTENSIONS:
                continue

            try:
                with open(fpath, 'r', errors='ignore') as f:
                    for line_num, line in enumerate(f, 1):
                        for pattern, desc in DEEP_SCAN_PATTERNS:
                            matches = re.findall(pattern, line)
                            for match in matches:
                                # Skip common false positives
                                if match in ('0x' + '0' * 64, '0x' + 'f' * 64):
                                    continue
                                # Skip if it's a variable reference
                                if match.startswith('$') or match.startswith('process.env'):
                                    continue
                                findings.append({
                                    "file": str(fpath),
                                    "line": line_num,
                                    "pattern": desc,
                                    "preview": match[:20] + '...' if len(match) > 20 else match,
                                })
            except (PermissionError, UnicodeDecodeError):
                continue

    return findings


def scan_locations(custom_paths: List[str] = None) -> List[Dict]:
    """Scan all common credential locations."""
    results = []
    home = Path.home()

    patterns = CREDENTIAL_PATTERNS.copy()
    if custom_paths:
        patterns.extend(custom_paths)

    # Expand patterns and check files
    checked = set()
    for pattern in patterns:
        expanded = home.glob(pattern.replace('~/', ''))
        for path in expanded:
            # Follow symlinks for existence check but report them
            if not (path.is_file() or path.is_symlink()) or str(path) in checked:
                continue
            checked.add(str(path))

            if path.suffix == '.json':
                result = scan_json_file(path)
            elif '.env' in path.name:
                result = scan_env_file(path)
            else:
                continue

            if result.get('likely_credentials'):
                results.append(result)

    # Check for existing .env
    env_path = home / '.openclaw' / '.env'
    if env_path.exists() and str(env_path) not in checked:
        results.append(scan_env_file(env_path))

    return results


def main():
    parser = argparse.ArgumentParser(description='Scan for credential files')
    parser.add_argument('--paths', nargs='+', help='Additional paths to scan')
    parser.add_argument('--deep', action='store_true',
                        help='Deep scan source files for hardcoded secrets')
    parser.add_argument('--format', choices=['text', 'json'], default='text',
                        help='Output format')
    args = parser.parse_args()

    results = scan_locations(args.paths)

    if args.format == 'json':
        output = {"files": results}
        if args.deep:
            output["deep_scan"] = deep_scan()
        print(json.dumps(output, indent=2))
    else:
        print(f"\n🔍 Found {len(results)} credential file(s):\n")
        for r in results:
            status = "✅" if r.get('mode') == '600' else "⚠️"
            print(f"{status} {r['path']}")
            print(f"   Type: {r['type']}")
            if r.get('symlink_target'):
                ok = "✅" if r.get('symlink_ok', True) else "⚠️"
                print(f"   {ok} Symlink → {r['symlink_target']}")
            if 'keys' in r:
                print(f"   Keys: {', '.join(r['keys'][:5])}")
                if len(r['keys']) > 5:
                    print(f"        (+{len(r['keys']) - 5} more)")
            print(f"   Mode: {r.get('mode', 'unknown')}")
            if r.get('mode') != '600':
                print(f"   ⚠️  Should be 600 for security")
            print()

        if args.deep:
            findings = deep_scan()
            if findings:
                print(f"\n🔎 Deep scan: {len(findings)} potential hardcoded secret(s):\n")
                for f in findings[:20]:
                    print(f"   ⚠️  {f['file']}:{f['line']}")
                    print(f"      Pattern: {f['pattern']}")
                    print(f"      Preview: {f['preview']}")
                    print()
                if len(findings) > 20:
                    print(f"   ... +{len(findings) - 20} more findings")
            else:
                print(f"\n🔎 Deep scan: No hardcoded secrets found ✅")

        print(f"\n📊 Summary:")
        print(f"   Total files: {len(results)}")
        print(f"   Insecure permissions: {sum(1 for r in results if r.get('mode') != '600')}")
        symlinks = [r for r in results if r.get('symlink_target')]
        if symlinks:
            print(f"   Symlinks: {len(symlinks)}")
        print(f"\n💡 Next: Run ./scripts/consolidate.py to merge into .env\n")


if __name__ == '__main__':
    main()


path: scripts/setup-gpg.sh
content: #!/usr/bin/env bash
set -euo pipefail

# GPG setup for OpenClaw credential encryption
# Usage: ./scripts/setup-gpg.sh [--cache-hours N]

CACHE_HOURS=8

# Parse args
while [[ $# -gt 0 ]]; do
    case $1 in
        --cache-hours)
            CACHE_HOURS="$2"
            shift 2
            ;;
        --help|-h)
            echo "Usage: $0 [--cache-hours N]"
            echo ""
            echo "Sets up GPG for OpenClaw credential encryption."
            echo ""
            echo "Options:"
            echo "  --cache-hours N   GPG agent cache timeout (default: 8)"
            echo "  --help, -h        Show this help"
            exit 0
            ;;
        *)
            echo "Unknown option: $1"
            exit 1
            ;;
    esac
done

CACHE_SECONDS=$((CACHE_HOURS * 3600))

echo ""
echo "🔐 OpenClaw GPG Setup"
echo "====================="
echo ""

# Step 1: Check if GPG is installed
echo "📋 Step 1: Checking GPG installation..."
if ! command -v gpg &>/dev/null; then
    echo "   ❌ GPG is not installed"
    echo ""
    echo "   Install with:"
    echo "     Ubuntu/Debian: sudo apt install gnupg"
    echo "     macOS:         brew install gnupg"
    echo "     Fedora:        sudo dnf install gnupg2"
    exit 1
fi

GPG_VERSION=$(gpg --version | head -1)
echo "   ✅ $GPG_VERSION"

# Step 2: Configure gpg-agent
echo ""
echo "📋 Step 2: Configuring GPG agent (cache: ${CACHE_HOURS}h)..."

GPG_DIR="$HOME/.gnupg"
mkdir -p "$GPG_DIR"
chmod 700 "$GPG_DIR"

AGENT_CONF="$GPG_DIR/gpg-agent.conf"

# Backup existing config
if [[ -f "$AGENT_CONF" ]]; then
    cp "$AGENT_CONF" "${AGENT_CONF}.bak"
    echo "   📦 Backed up existing config"
fi

# Write agent config
cat > "$AGENT_CONF" << EOF
# OpenClaw GPG Agent Configuration
# Cache passphrase for ${CACHE_HOURS} hours
default-cache-ttl ${CACHE_SECONDS}
max-cache-ttl ${CACHE_SECONDS}

# Allow loopback pinentry (for headless/script usage)
allow-loopback-pinentry
EOF

chmod 600 "$AGENT_CONF"

# Configure GPG to use loopback pinentry
GPG_CONF="$GPG_DIR/gpg.conf"
if ! grep -q "pinentry-mode loopback" "$GPG_CONF" 2>/dev/null; then
    echo "pinentry-mode loopback" >> "$GPG_CONF"
fi

echo "   ✅ Agent configured (cache: ${CACHE_HOURS}h)"

# Reload agent
gpgconf --kill gpg-agent 2>/dev/null || true
echo "   ✅ Agent reloaded"

# Step 3: Test encrypt/decrypt cycle
echo ""
echo "📋 Step 3: Testing encrypt/decrypt cycle..."

TEST_FILE=$(mktemp)
TEST_ENC="${TEST_FILE}.gpg"
TEST_DEC="${TEST_FILE}.dec"
TEST_DATA="openclaw-gpg-test-$(date +%s)"

echo "$TEST_DATA" > "$TEST_FILE"

# Try encryption
if gpg -c --batch --yes --cipher-algo AES256 --passphrase "" -o "$TEST_ENC" "$TEST_FILE" 2>/dev/null; then
    # Try decryption
    if gpg -d --batch --quiet --passphrase "" "$TEST_ENC" > "$TEST_DEC" 2>/dev/null; then
        DECRYPTED=$(cat "$TEST_DEC")
        if [[ "$DECRYPTED" == "$TEST_DATA" ]]; then
            echo "   ✅ Encrypt/decrypt test passed"
        else
            echo "   ⚠️  Decrypt returned different data"
        fi
    else
        echo "   ⚠️  Decryption test failed (passphrase may be needed)"
        echo "   💡 This is normal — you'll set a passphrase when encrypting secrets"
    fi
else
    echo "   ⚠️  Encryption test failed"
    echo "   💡 GPG may need additional configuration"
fi

# Cleanup test files
rm -f "$TEST_FILE" "$TEST_ENC" "$TEST_DEC"

# Step 4: Summary
echo ""
echo "✅ GPG setup complete!"
echo ""
echo "   📁 GPG home: $GPG_DIR"
echo "   ⏱️  Cache TTL: ${CACHE_HOURS} hours"
echo "   🔐 Cipher: AES256"
echo ""
echo "💡 Next steps:"
echo "   # Encrypt high-value keys:"
echo "   ./scripts/encrypt.py --keys MAIN_WALLET_PRIVATE_KEY,CUSTODY_PRIVATE_KEY"
echo ""
echo "   # List encrypted keys:"
echo "   ./scripts/encrypt.py --list"
echo ""


path: scripts/validate.py
content: #!/usr/bin/env python3
"""
Validate .env file security, format, and credential strength.
Checks permissions, entropy, private key exposure, backup security.
"""

import argparse
import math
import os
import re
from pathlib import Path
from typing import Dict, List


def calc_entropy(s: str) -> float:
    """Calculate Shannon entropy of a string in bits per character."""
    if not s:
        return 0.0
    freq = {}
    for c in s:
        freq[c] = freq.get(c, 0) + 1
    length = len(s)
    return -sum((count / length) * math.log2(count / length) for count in freq.values())


def check_permissions(env_file: Path) -> Dict:
    """Check file permissions."""
    if not env_file.exists():
        return {'status': 'missing', 'message': 'File does not exist'}

    mode = oct(env_file.stat().st_mode)[-3:]

    if mode == '600':
        return {'status': 'ok', 'mode': mode}
    else:
        return {
            'status': 'insecure',
            'mode': mode,
            'message': f'Permissions {mode} are too permissive (should be 600)'
        }


def check_gitignore(openclaw_dir: Path) -> Dict:
    """Check if .env is in .gitignore."""
    gitignore = openclaw_dir / '.gitignore'

    if not gitignore.exists():
        return {'status': 'missing', 'message': '.gitignore does not exist'}

    content = gitignore.read_text()
    if '.env' in content or '*.env' in content:
        return {'status': 'ok'}
    else:
        return {'status': 'unprotected', 'message': '.env not in .gitignore'}


def check_format(env_file: Path) -> Dict:
    """Check .env file format."""
    issues = []
    keys = set()
    duplicates = []

    with open(env_file) as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()

            # Skip comments and empty lines
            if not line or line.startswith('#'):
                continue

            # Check format
            if '=' not in line:
                issues.append(f"Line {line_num}: Missing '=' separator")
                continue

            key, value = line.split('=', 1)
            key = key.strip()

            # Check key format
            if not re.match(r'^[A-Z0-9_]+$', key):
                issues.append(f"Line {line_num}: Invalid key format '{key}'")

            # Check for duplicates
            if key in keys:
                duplicates.append(key)
            keys.add(key)

            # Values with spaces that aren't quoted — warn
            if ' ' in value and not (value.startswith('"') or value.startswith("'")):
                # Allow JSON-like values (starts with { or [)
                if not (value.startswith('{') or value.startswith('[')):
                    issues.append(f"Line {line_num}: Value with spaces should be quoted (key: {key})")

    if issues or duplicates:
        return {
            'status': 'issues',
            'issues': issues,
            'duplicates': duplicates,
            'keys_count': len(keys)
        }
    else:
        return {
            'status': 'ok',
            'keys_count': len(keys)
        }


def check_security(env_file: Path) -> Dict:
    """Check for security issues including entropy, private keys, mnemonics."""
    warnings = []

    with open(env_file) as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line or line.startswith('#') or '=' not in line:
                continue

            key, value = line.split('=', 1)
            key = key.strip()
            value = value.strip().strip('"').strip("'")

            # Skip GPG-encrypted placeholders
            if value.startswith('GPG:'):
                continue

            # Check for private key patterns in values (not in ADDRESS/HASH keys)
            key_upper = key.upper()
            if re.match(r'^0x[a-fA-F0-9]{64}$', value):
                if not any(x in key_upper for x in ['ADDRESS', 'HASH', 'TX', 'CONTRACT']):
                    warnings.append(
                        f"Line {line_num}: {key} looks like a private key (0x + 64 hex). "
                        f"Consider GPG encryption: ./scripts/encrypt.py --keys {key}"
                    )

            # Check for mnemonic patterns (12 or 24 words)
            words = value.split()
            if len(words) in (12, 24) and all(w.isalpha() and w.islower() for w in words):
                warnings.append(
                    f"Line {line_num}: {key} looks like a mnemonic seed phrase ({len(words)} words). "
                    f"Consider GPG encryption: ./scripts/encrypt.py --keys {key}"
                )

            # Entropy check for keys that should be secret
            secret_indicators = ['SECRET', 'PRIVATE_KEY', 'MNEMONIC', 'PASSWORD', 'PASSPHRASE']
            if any(ind in key_upper for ind in secret_indicators):
                if value and len(value) > 4:
                    entropy = calc_entropy(value)
                    if entropy < 3.0:
                        warnings.append(
                            f"Line {line_num}: {key} has low entropy ({entropy:.1f} bits/char). "
                            f"May be a weak or placeholder value."
                        )

            # Check for common weak values
            weak_values = ['password', 'password123', 'test', 'changeme', 'admin', 'default',
                           'your_value_here', 'xxx', 'placeholder']
            if value.lower() in weak_values:
                warnings.append(f"Line {line_num}: {key} has a weak/placeholder value")

    return {
        'status': 'ok' if not warnings else 'warnings',
        'warnings': warnings
    }


def check_backups(openclaw_dir: Path) -> Dict:
    """Check backup file and directory permissions."""
    issues = []
    backup_dir = openclaw_dir / 'backups'

    if not backup_dir.exists():
        return {'status': 'ok', 'message': 'No backups directory'}

    # Check backup root dir
    mode = oct(backup_dir.stat().st_mode)[-3:]
    if mode != '700':
        issues.append(f"{backup_dir}: directory mode {mode} (should be 700)")

    # Check each backup subdirectory and files
    for sub in backup_dir.iterdir():
        if sub.is_dir():
            sub_mode = oct(sub.stat().st_mode)[-3:]
            if sub_mode != '700':
                issues.append(f"{sub}: directory mode {sub_mode} (should be 700)")

            for f in sub.iterdir():
                if f.is_file():
                    f_mode = oct(f.stat().st_mode)[-3:]
                    if f_mode != '600':
                        issues.append(f"{f.name}: file mode {f_mode} (should be 600)")

    return {
        'status': 'ok' if not issues else 'issues',
        'issues': issues
    }


def fix_permissions(env_file: Path):
    """Fix .env file permissions."""
    os.chmod(env_file, 0o600)
    print(f"   🔧 Fixed permissions: 600")


def fix_gitignore(openclaw_dir: Path):
    """Add .env to .gitignore."""
    gitignore = openclaw_dir / '.gitignore'
    entries = ['.env', '.env.secrets.gpg', '.env.meta']

    if not gitignore.exists():
        with open(gitignore, 'w') as f:
            f.write("# Credentials\n")
            for e in entries:
                f.write(f"{e}\n")
    else:
        content = gitignore.read_text()
        missing = [e for e in entries if e not in content]
        if missing:
            with open(gitignore, 'a') as f:
                f.write("\n# Credentials\n")
                for e in missing:
                    f.write(f"{e}\n")

    print(f"   🔧 Updated .gitignore")


def fix_backups(openclaw_dir: Path):
    """Fix backup file and directory permissions."""
    backup_dir = openclaw_dir / 'backups'
    if not backup_dir.exists():
        return

    fixed = 0
    os.chmod(backup_dir, 0o700)

    for sub in backup_dir.iterdir():
        if sub.is_dir():
            os.chmod(sub, 0o700)
            fixed += 1
            for f in sub.iterdir():
                if f.is_file():
                    os.chmod(f, 0o600)
                    fixed += 1

    print(f"   🔧 Fixed {fixed} backup file/directory permissions")


def validate(check_type: str = 'all', auto_fix: bool = False) -> bool:
    """Validate .env file."""
    home = Path.home()
    openclaw_dir = home / '.openclaw'
    env_file = openclaw_dir / '.env'

    print("\n🔍 Validating credentials...\n")

    all_ok = True

    # Check permissions
    if check_type in ['all', 'permissions']:
        print("📋 Checking permissions...")
        result = check_permissions(env_file)
        if result['status'] == 'ok':
            print(f"   ✅ Permissions: {result['mode']}")
        elif result['status'] == 'missing':
            print(f"   ❌ {result['message']}")
            return False
        else:
            print(f"   ⚠️  {result['message']}")
            all_ok = False
            if auto_fix:
                fix_permissions(env_file)
                all_ok = True

    # Check gitignore
    if check_type in ['all', 'gitignore']:
        print("\n📋 Checking .gitignore...")
        result = check_gitignore(openclaw_dir)
        if result['status'] == 'ok':
            print(f"   ✅ .env is git-ignored")
        else:
            print(f"   ⚠️  {result.get('message', 'Not protected')}")
            all_ok = False
            if auto_fix:
                fix_gitignore(openclaw_dir)
                all_ok = True

    # Check format
    if check_type in ['all', 'format']:
        print("\n📋 Checking format...")
        result = check_format(env_file)
        if result['status'] == 'ok':
            print(f"   ✅ Format valid ({result['keys_count']} keys)")
        else:
            if result['issues']:
                print(f"   ⚠️  Found {len(result['issues'])} issue(s):")
                for issue in result['issues'][:5]:
                    print(f"      • {issue}")
                if len(result['issues']) > 5:
                    print(f"      ... +{len(result['issues']) - 5} more")
            if result['duplicates']:
                print(f"   ⚠️  Duplicate keys: {', '.join(result['duplicates'])}")
            all_ok = False

    # Check security
    if check_type in ['all', 'security']:
        print("\n📋 Checking security...")
        result = check_security(env_file)
        if result['status'] == 'ok':
            print(f"   ✅ No security warnings")
        else:
            print(f"   ⚠️  Found {len(result['warnings'])} warning(s):")
            for warning in result['warnings'][:10]:
                print(f"      • {warning}")
            if len(result['warnings']) > 10:
                print(f"      ... +{len(result['warnings']) - 10} more")

    # Check backups
    if check_type in ['all', 'backups']:
        print("\n📋 Checking backup security...")
        result = check_backups(openclaw_dir)
        if result['status'] == 'ok':
            print(f"   ✅ Backup permissions OK")
        else:
            print(f"   ⚠️  Found {len(result['issues'])} issue(s):")
            for issue in result['issues'][:10]:
                print(f"      • {issue}")
            if len(result['issues']) > 10:
                print(f"      ... +{len(result['issues']) - 10} more")
            all_ok = False
            if auto_fix:
                fix_backups(openclaw_dir)
                all_ok = True

    # Summary
    print(f"\n{'✅' if all_ok else '⚠️'} Validation {'passed' if all_ok else 'found issues'}")

    if not all_ok and not auto_fix:
        print(f"\n💡 Run with --fix to automatically fix issues")

    return all_ok


def main():
    parser = argparse.ArgumentParser(description='Validate credentials')
    parser.add_argument('--check', choices=['all', 'permissions', 'gitignore', 'format', 'security', 'backups'],
                        default='all', help='What to check')
    parser.add_argument('--fix', action='store_true',
                        help='Automatically fix issues')
    args = parser.parse_args()

    result = validate(args.check, args.fix)
    return 0 if result else 1


if __name__ == '__main__':
    exit(main())


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