openclaw-skills/env-secrets-manager/SKILL.md
Complete environment and secrets management workflow: .env file lifecycle across dev/staging/prod, .env.example auto-generation, required-var validation, secret leak detection in git history, and credential rotation playbook. Integrates with HashiCorp Vault, AWS SSM, 1Password CLI, and Doppler.
npx skillsauth add seaworld008/commonly-used-high-value-skills env-secrets-managerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Tier: POWERFUL Category: Engineering Domain: Security / DevOps / Configuration Management
Complete environment and secrets management workflow: .env file lifecycle across dev/staging/prod, .env.example auto-generation, required-var validation, secret leak detection in git history, and credential rotation playbook. Integrates with HashiCorp Vault, AWS SSM, 1Password CLI, and Doppler.
# .env.example — committed to git (no values)
# .env.local — developer machine (gitignored)
# .env.staging — CI/CD or secret manager reference
# .env.prod — never on disk; pulled from secret manager at runtime
# Application
APP_NAME=
APP_ENV= # dev | staging | prod
APP_PORT=3000 # default port if not set
APP_SECRET= # REQUIRED: JWT signing secret (min 32 chars)
APP_URL= # REQUIRED: public base URL
# Database
DATABASE_URL= # REQUIRED: full connection string
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10
# Auth
AUTH_JWT_SECRET= # REQUIRED
AUTH_JWT_EXPIRY=3600 # seconds
AUTH_REFRESH_SECRET= # REQUIRED
# Third-party APIs
STRIPE_SECRET_KEY= # REQUIRED in prod
STRIPE_WEBHOOK_SECRET= # REQUIRED in prod
SENDGRID_API_KEY=
# Storage
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=eu-central-1
AWS_S3_BUCKET=
# Monitoring
SENTRY_DSN=
DD_API_KEY=
Add to your project's .gitignore:
# Environment files — NEVER commit these
.env
.env.local
.env.development
.env.development.local
.env.test.local
.env.staging
.env.staging.local
.env.production
.env.production.local
.env.prod
.env.*.local
# Secret files
*.pem
*.key
*.p12
*.pfx
secrets.json
secrets.yaml
secrets.yml
credentials.json
service-account.json
# AWS
.aws/credentials
# Terraform state (may contain secrets)
*.tfstate
*.tfstate.backup
.terraform/
# Kubernetes secrets
*-secret.yaml
*-secrets.yaml
#!/bin/bash
# scripts/gen-env-example.sh
# Strips values from .env, preserves keys, defaults, and comments
INPUT="${1:-.env}"
OUTPUT="${2:-.env.example}"
if [ ! -f "$INPUT" ]; then
echo "ERROR: $INPUT not found"
exit 1
fi
python3 - "$INPUT" "$OUTPUT" << 'PYEOF'
import sys, re
input_file = sys.argv[1]
output_file = sys.argv[2]
lines = []
with open(input_file) as f:
for line in f:
stripped = line.rstrip('\n')
# Keep blank lines and comments as-is
if stripped == '' or stripped.startswith('#'):
lines.append(stripped)
continue
# Match KEY=VALUE or KEY="VALUE"
m = re.match(r'^([A-Z_][A-Z0-9_]*)=(.*)$', stripped)
if m:
key = m.group(1)
value = m.group(2).strip('"\'')
# Keep non-sensitive defaults (ports, regions, feature flags)
safe_defaults = re.compile(
r'^(APP_PORT|APP_ENV|APP_NAME|AWS_REGION|DATABASE_POOL_|LOG_LEVEL|'
r'FEATURE_|CACHE_TTL|RATE_LIMIT_|PAGINATION_|TIMEOUT_)',
re.I
)
sensitive = re.compile(
r'(SECRET|KEY|TOKEN|PASSWORD|PASS|CREDENTIAL|DSN|AUTH|PRIVATE|CERT)',
re.I
)
if safe_defaults.match(key) and value:
lines.append(f"{key}={value} # default")
else:
lines.append(f"{key}=")
else:
lines.append(stripped)
with open(output_file, 'w') as f:
f.write('\n'.join(lines) + '\n')
print(f"Generated {output_file} from {input_file}")
PYEOF
Usage:
bash scripts/gen-env-example.sh .env .env.example
# Commit .env.example, never .env
git add .env.example
#!/bin/bash
# scripts/validate-env.sh
# Run at app startup or in CI before deploy
# Exit 1 if any required var is missing or empty
set -euo pipefail
MISSING=()
WARNINGS=()
# --- Define required vars by environment ---
ALWAYS_REQUIRED=(
APP_SECRET
APP_URL
DATABASE_URL
AUTH_JWT_SECRET
AUTH_REFRESH_SECRET
)
PROD_REQUIRED=(
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
SENTRY_DSN
)
# --- Check always-required vars ---
for var in "${ALWAYS_REQUIRED[@]}"; do
if [ -z "${!var:-}" ]; then
MISSING+=("$var")
fi
done
# --- Check prod-only vars ---
if [ "${APP_ENV:-}" = "production" ] || [ "${NODE_ENV:-}" = "production" ]; then
for var in "${PROD_REQUIRED[@]}"; do
if [ -z "${!var:-}" ]; then
MISSING+=("$var (required in production)")
fi
done
fi
# --- Validate format/length constraints ---
if [ -n "${AUTH_JWT_SECRET:-}" ] && [ ${#AUTH_JWT_SECRET} -lt 32 ]; then
WARNINGS+=("AUTH_JWT_SECRET is shorter than 32 chars — insecure")
fi
if [ -n "${DATABASE_URL:-}" ]; then
if ! echo "$DATABASE_URL" | grep -qE "^(postgres|postgresql|mysql|mongodb|redis)://"; then
WARNINGS+=("DATABASE_URL doesn't look like a valid connection string")
fi
fi
if [ -n "${APP_PORT:-}" ]; then
if ! [[ "$APP_PORT" =~ ^[0-9]+$ ]] || [ "$APP_PORT" -lt 1 ] || [ "$APP_PORT" -gt 65535 ]; then
WARNINGS+=("APP_PORT=$APP_PORT is not a valid port number")
fi
fi
# --- Report ---
if [ ${#WARNINGS[@]} -gt 0 ]; then
echo "WARNINGS:"
for w in "${WARNINGS[@]}"; do
echo " ⚠️ $w"
done
fi
if [ ${#MISSING[@]} -gt 0 ]; then
echo ""
echo "FATAL: Missing required environment variables:"
for var in "${MISSING[@]}"; do
echo " ❌ $var"
done
echo ""
echo "Copy .env.example to .env and fill in missing values."
exit 1
fi
echo "✅ All required environment variables are set"
Node.js equivalent:
// src/config/validateEnv.ts
const required = [
'APP_SECRET', 'APP_URL', 'DATABASE_URL',
'AUTH_JWT_SECRET', 'AUTH_REFRESH_SECRET',
]
const missing = required.filter(key => !process.env[key])
if (missing.length > 0) {
console.error('FATAL: Missing required environment variables:', missing)
process.exit(1)
}
if (process.env.AUTH_JWT_SECRET && process.env.AUTH_JWT_SECRET.length < 32) {
console.error('FATAL: AUTH_JWT_SECRET must be at least 32 characters')
process.exit(1)
}
export const config = {
appSecret: process.env.APP_SECRET!,
appUrl: process.env.APP_URL!,
databaseUrl: process.env.DATABASE_URL!,
jwtSecret: process.env.AUTH_JWT_SECRET!,
refreshSecret: process.env.AUTH_REFRESH_SECRET!,
stripeKey: process.env.STRIPE_SECRET_KEY, // optional
port: parseInt(process.env.APP_PORT ?? '3000', 10),
} as const
#!/bin/bash
# scripts/scan-secrets.sh
# Scan staged files and working tree for common secret patterns
FAIL=0
check() {
local label="$1"
local pattern="$2"
local matches
matches=$(git diff --cached -U0 2>/dev/null | grep "^+" | grep -vE "^(\+\+\+|#|\/\/)" | \
grep -E "$pattern" | grep -v ".env.example" | grep -v "test\|mock\|fixture\|fake" || true)
if [ -n "$matches" ]; then
echo "SECRET DETECTED [$label]:"
echo "$matches" | head -5
FAIL=1
fi
}
# AWS Access Keys
check "AWS Access Key" "AKIA[0-9A-Z]{16}"
check "AWS Secret Key" "aws_secret_access_key\s*=\s*['\"]?[A-Za-z0-9/+]{40}"
# Stripe
check "Stripe Live Key" "sk_live_[0-9a-zA-Z]{24,}"
check "Stripe Test Key" "sk_test_[0-9a-zA-Z]{24,}"
check "Stripe Webhook" "whsec_[0-9a-zA-Z]{32,}"
# JWT / Generic secrets
check "Hardcoded JWT" "eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}"
check "Generic Secret" "(secret|password|passwd|api_key|apikey|token)\s*[:=]\s*['\"][^'\"]{12,}['\"]"
# Private keys
check "Private Key Block" "-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"
check "PEM Certificate" "-----BEGIN CERTIFICATE-----"
# Connection strings with credentials
check "DB Connection" "(postgres|mysql|mongodb)://[^:]+:[^@]+@"
check "Redis Auth" "redis://:[^@]+@\|rediss://:[^@]+@"
# Google
check "Google API Key" "AIza[0-9A-Za-z_-]{35}"
check "Google OAuth" "[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com"
# GitHub
check "GitHub Token" "gh[ps]_[A-Za-z0-9]{36,}"
check "GitHub Fine-grained" "github_pat_[A-Za-z0-9_]{82}"
# Slack
check "Slack Token" "xox[baprs]-[0-9A-Za-z]{10,}"
check "Slack Webhook" "https://hooks\.slack\.com/services/[A-Z0-9]{9,}/[A-Z0-9]{9,}/[A-Za-z0-9]{24,}"
# Twilio
check "Twilio SID" "AC[a-z0-9]{32}"
check "Twilio Token" "SK[a-z0-9]{32}"
if [ $FAIL -eq 1 ]; then
echo ""
echo "BLOCKED: Secrets detected in staged changes."
echo "Remove secrets before committing. Use environment variables instead."
echo "If this is a false positive, add it to .secretsignore or use:"
echo " git commit --no-verify (only if you're 100% certain it's safe)"
exit 1
fi
echo "No secrets detected in staged changes."
#!/bin/bash
# scripts/scan-history.sh — scan entire git history for leaked secrets
PATTERNS=(
"AKIA[0-9A-Z]{16}"
"sk_live_[0-9a-zA-Z]{24}"
"sk_test_[0-9a-zA-Z]{24}"
"-----BEGIN.*PRIVATE KEY-----"
"AIza[0-9A-Za-z_-]{35}"
"ghp_[A-Za-z0-9]{36}"
"xox[baprs]-[0-9A-Za-z]{10,}"
)
for pattern in "${PATTERNS[@]}"; do
echo "Scanning for: $pattern"
git log --all -p --no-color 2>/dev/null | \
grep -n "$pattern" | \
grep "^+" | \
grep -v "^+++" | \
head -10
done
# Alternative: use truffleHog or gitleaks for comprehensive scanning
# gitleaks detect --source . --log-opts="--all"
# trufflehog git file://. --only-verified
#!/bin/bash
# Install the pre-commit hook
HOOK_PATH=".git/hooks/pre-commit"
cat > "$HOOK_PATH" << 'HOOK'
#!/bin/bash
# Pre-commit: scan for secrets before every commit
SCRIPT="scripts/scan-secrets.sh"
if [ -f "$SCRIPT" ]; then
bash "$SCRIPT"
else
# Inline fallback if script not present
if git diff --cached -U0 | grep "^+" | grep -qE "AKIA[0-9A-Z]{16}|sk_live_|-----BEGIN.*PRIVATE KEY"; then
echo "BLOCKED: Possible secret detected in staged changes."
exit 1
fi
fi
HOOK
chmod +x "$HOOK_PATH"
echo "Pre-commit hook installed at $HOOK_PATH"
Using pre-commit framework (recommended for teams):
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- repo: local
hooks:
- id: validate-env-example
name: Check .env.example is up to date
language: script
entry: bash scripts/check-env-example.sh
pass_filenames: false
When a secret is leaked or compromised:
# Confirm which secret was exposed
git log --all -p --no-color | grep -A2 -B2 "AKIA\|sk_live_\|SECRET"
# Check if secret is in any open PRs
gh pr list --state open | while read pr; do
gh pr diff $(echo $pr | awk '{print $1}') | grep -E "AKIA|sk_live_" && echo "Found in PR: $pr"
done
# Find first commit that introduced the secret
git log --all -p --no-color -- "*.env" "*.json" "*.yaml" "*.ts" "*.py" | \
grep -B 10 "THE_LEAKED_VALUE" | grep "^commit" | tail -1
# Get commit date
git show --format="%ci" COMMIT_HASH | head -1
# Check if secret appears in public repos (GitHub)
gh api search/code -X GET -f q="THE_LEAKED_VALUE" | jq '.total_count, .items[].html_url'
Per service — rotate immediately:
ALTER USER app_user PASSWORD 'new-strong-password-here';# Update secret manager (source of truth)
# Then redeploy to pull new values
# Vault KV v2
vault kv put secret/myapp/prod \
STRIPE_SECRET_KEY="sk_live_NEW..." \
APP_SECRET="new-secret-here"
# AWS SSM
aws ssm put-parameter \
--name "/myapp/prod/STRIPE_SECRET_KEY" \
--value "sk_live_NEW..." \
--type "SecureString" \
--overwrite
# 1Password
op item edit "MyApp Prod" \
--field "STRIPE_SECRET_KEY[password]=sk_live_NEW..."
# Doppler
doppler secrets set STRIPE_SECRET_KEY="sk_live_NEW..." --project myapp --config prod
# WARNING: rewrites history — coordinate with team first
git filter-repo --path-glob "*.env" --invert-paths
# Or remove specific string from all commits
git filter-repo --replace-text <(echo "LEAKED_VALUE==>REDACTED")
# Force push all branches (requires team coordination + force push permissions)
git push origin --force --all
# Notify all developers to re-clone
# Confirm secret no longer in history
git log --all -p | grep "LEAKED_VALUE" | wc -l # should be 0
# Test new credentials work
curl -H "Authorization: Bearer $NEW_TOKEN" https://api.service.com/test
# Monitor for unauthorized usage of old credential (check service audit logs)
# Setup
export VAULT_ADDR="https://vault.internal.company.com"
export VAULT_TOKEN="$(vault login -method=oidc -format=json | jq -r '.auth.client_token')"
# Write secrets
vault kv put secret/myapp/prod \
DATABASE_URL="postgres://user:pass@host/db" \
APP_SECRET="$(openssl rand -base64 32)"
# Read secrets into env
eval $(vault kv get -format=json secret/myapp/prod | \
jq -r '.data.data | to_entries[] | "export \(.key)=\(.value)"')
# In CI/CD (GitHub Actions)
# Use vault-action: hashicorp/vault-action@v2
# Write (SecureString = encrypted with KMS)
aws ssm put-parameter \
--name "/myapp/prod/DATABASE_URL" \
--value "postgres://..." \
--type "SecureString" \
--key-id "alias/myapp-secrets"
# Read all params for an app/env into shell
eval $(aws ssm get-parameters-by-path \
--path "/myapp/prod/" \
--with-decryption \
--query "Parameters[*].[Name,Value]" \
--output text | \
awk '{split($1,a,"/"); print "export " a[length(a)] "=\"" $2 "\""}')
# In Node.js at startup
# Use @aws-sdk/client-ssm to pull params before server starts
# Authenticate
eval $(op signin)
# Get a specific field
op read "op://MyVault/MyApp Prod/STRIPE_SECRET_KEY"
# Export all fields from an item as env vars
op item get "MyApp Prod" --format json | \
jq -r '.fields[] | select(.value != null) | "export \(.label)=\"\(.value)\""' | \
grep -E "^export [A-Z_]+" | source /dev/stdin
# .env injection
op inject -i .env.tpl -o .env
# .env.tpl uses {{ op://Vault/Item/field }} syntax
# Setup
doppler setup # interactive: select project + config
# Run any command with secrets injected
doppler run -- node server.js
doppler run -- npm run dev
# Export to .env (local dev only — never commit output)
doppler secrets download --no-file --format env > .env.local
# Pull specific secret
doppler secrets get DATABASE_URL --plain
# Sync to another environment
doppler secrets upload --project myapp --config staging < .env.staging.example
Check if staging and prod have the same set of keys (values may differ):
#!/bin/bash
# scripts/check-env-drift.sh
# Pull key names from both environments (not values)
STAGING_KEYS=$(doppler secrets --project myapp --config staging --format json 2>/dev/null | \
jq -r 'keys[]' | sort)
PROD_KEYS=$(doppler secrets --project myapp --config prod --format json 2>/dev/null | \
jq -r 'keys[]' | sort)
ONLY_IN_STAGING=$(comm -23 <(echo "$STAGING_KEYS") <(echo "$PROD_KEYS"))
ONLY_IN_PROD=$(comm -13 <(echo "$STAGING_KEYS") <(echo "$PROD_KEYS"))
if [ -n "$ONLY_IN_STAGING" ]; then
echo "Keys in STAGING but NOT in PROD:"
echo "$ONLY_IN_STAGING" | sed 's/^/ /'
fi
if [ -n "$ONLY_IN_PROD" ]; then
echo "Keys in PROD but NOT in STAGING:"
echo "$ONLY_IN_PROD" | sed 's/^/ /'
fi
if [ -z "$ONLY_IN_STAGING" ] && [ -z "$ONLY_IN_PROD" ]; then
echo "✅ No env drift detected — staging and prod have identical key sets"
fi
.env to .gitignore on day 1; use pre-commit hooksecho $SECRET; mask vars in CI settingsAPP_SECRET=mysecret is not a secret. Use openssl rand -base64 32testing
Orchestrating specialist AI agent teams as a meta-coordinator. Decomposes requests into minimum viable chains, spawns each as an independent session in AUTORUN modes, and drives to final output. Use when a task spans multiple specialist domains, requires parallel agent execution, or needs hub-and-spoke routing across the skill ecosystem.
tools
用于 Next.js App Router 模式开发,包含 RSC、Server Actions 和路由最佳实践。来源:skills.sh 10.2K installs。
tools
Deploy web projects to Netlify using the Netlify CLI (`npx netlify`). Use when the user asks to deploy, host, publish, or link a site/repo on Netlify, including preview and production deploys.
tools
Guides and best practices for working with Neon Serverless Postgres. Covers setup, connection methods, branching, autoscaling, scale-to-zero, read replicas, connection pooling, Neon Auth, and the Neon CLI, MCP server, REST API, TypeScript SDK, and Python SDK. Use when users ask about "Neon setup", "connect to Neon", "Neon project", "DATABASE_URL", "serverless Postgres", "Neon CLI", "neonctl", "Neon MCP", "Neon Auth", "@neondatabase/serverless", "@neondatabase/neon-js", "scale to zero", "Neon autoscaling", "Neon read replica", or "Neon connection pooling".