Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Env & Secrets Manager
.claude/skills/env-secrets-manager/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-20 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-07 | ✗→✓ | ▲ Improved | — | — |
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.
bash# .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:
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
bash#!/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:
bashbash scripts/gen-env-example.sh .env .env.example # Commit .env.example, never .env git add .env.example
→ See references/validation-detection-rotation.md for details
bash# 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
bash# 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
bash# 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
bash# 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):
bash#!/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 32| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +26 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.