Skip to content

env-ninja

Use when hardcoded secrets or API keys are detected in source code, when organizing .env files, when centralizing environment variables, or when the user says ‘env ninja’, ‘organize env’, ‘hardcoded secret’, ‘API key in code’, ‘.env cleanup’. Also use when armadillo-sync detects scattered secrets.

ModelSourceCategory
sonnetcoreData Quality

Context: fork

Full Reference

Centralized environment variable enforcement. Scans for hardcoded secrets, organizes .env files with sections and comments, replaces hardcoded values with process.env references, and enables ongoing enforcement.

Mandatory Announcement — FIRST OUTPUT before anything else:

┏━ 🔧 env-ninja ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [one-line description of what you're doing] ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

No exceptions. Box frame first, then work.

ENV-Ninja uses pattern-based detection because secret values can’t be read from a committed reference file (unlike NAP-Ninja which matches exact values from business.json).

PatternDetectionExample
Database URLs(postgres|mysql|mongodb|redis):// with credentials"postgresql://user:pass@host:5432/db"
Auth in URLs://[^:]+:[^@]+@https://admin:secret@host.com
AWS keysAKIA[0-9A-Z]{16}AWS access key IDs
Stripe keyssk_live_, pk_live_, sk_test_, rk_live_Stripe secret/publishable keys
JWT tokenseyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+Inline JWTs
PEM keys-----BEGIN.*PRIVATE KEY-----Inline private keys
API keysLong alphanumeric (20+) assigned to key, secret, token, password varsconst API_KEY = "sk_abc123..."
Generic secretsHex 32+ or base64 40+ assigned to secret-named varsSECRET = "a1b2c3d4..."
  • .env* files — secrets belong there
  • Test files (*.test.*, *.spec.*, __tests__/) — fake values are fine
  • node_modules/, .git/, dist/, build/, lock files
  • Known false positives: CSS hex colors, UUIDs in test seeds, git SHAs
Terminal window
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# App
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NODE_ENV=development
PORT=3000
APP_URL=http://localhost:3000
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Database
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DATABASE_URL=
DIRECT_URL=
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Auth
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Third-Party APIs
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY=
RESEND_API_KEY=
SectionVariable patterns
AppNODE_ENV, PORT, APP_*, NEXT_PUBLIC_APP_*
DatabaseDATABASE_*, DB_*, DIRECT_URL
Auth*AUTH*, JWT_*, SESSION_*, COOKIE_*
Third-Party APIsExternal service keys not in other categories
Analytics & MonitoringPOSTHOG_*, SENTRY_*, GA_*
StorageS3_*, R2_*, CLOUDINARY_*, UPLOADTHING_*
Feature FlagsFEATURE_*, FF_*, ENABLE_*
MiscEverything uncategorized
FilePurposeIn .gitignore?ENV-Ninja writes?
.env.exampleTemplate — empty secrets, safe defaultsNo — committed✓ Always generates
.envDev valuesYes✓ Creates from .example if missing
.env.localPersonal overridesYes✗ Never touches
.env.productionProduction (platform-set)Yes✗ Never touches

Critical: Never write actual secret values. Only write structure (names, sections, comments) and safe defaults.

  1. Grep codebase for hardcoded secret patterns (see Detection Patterns)
  2. Check existing .env* files — organized? .env.example current?
  3. Check .gitignore.env* entries present?
  4. Present findings table:
    • Hardcoded secrets: N instances in M files
    • .env.example status: missing / outdated / current
    • .gitignore status: ✓ or ✗
  5. Ask: “Want me to fix all of this?”
  1. Collect all variable names from .env files and process.env.* / import.meta.env.* in source
  2. Sort into sections by category (see Section Categories)
  3. Generate .env.example with section dividers and empty secret values
  4. Update .env with same structure — preserve existing values
  5. Ensure .gitignore has .env, .env.local, .env.*.local entries
  6. Commit: chore(env-ninja): organize .env files and generate .env.example

For each hardcoded secret found:

  1. Propose a variable name (e.g., STRIPE_SECRET_KEY, DATABASE_URL)
  2. Add variable to .env.example
  3. Replace hardcoded value with framework-appropriate reference

Framework-aware replacement:

FrameworkPattern
Node.js / Next.jsprocess.env.VARIABLE_NAME
Vite / React+Viteimport.meta.env.VITE_VARIABLE_NAME
Astroimport.meta.env.VARIABLE_NAME
Django / Pythonos.environ.get('VARIABLE_NAME')
Any otherprocess.env.VARIABLE_NAME
  1. Commit per group: refactor(env-ninja): move database URL to env var
  1. Re-scan for remaining hardcoded secrets
  2. Verify .gitignore has all .env* entries
  3. Verify .env.example has an entry for every process.env.* call in source
  4. Report: 0 hardcoded secrets remain or flag remaining
  5. Run project test suite
  1. Confirm hook is active
  2. Tell user:
    ENV-Ninja is watching. Future writes that contain hardcoded
    secrets will get flagged.
    ▸ To pause: "turn off env-ninja" or set
    envNinja: false in .claude/settings.json
    ▸ To rescan: /env-ninja
ActionWhat happens
”turn off env-ninja”Set envNinja: false in .claude/settings.json → hook silences
”turn on env-ninja” / /env-ninjaRemove envNinja: false → quick scan → re-enabled

To disable: set envNinja: false in .claude/settings.json to pause the hook.

MistakeFix
Writing actual secrets to .env.exampleOnly write empty values or safe defaults
Missing .gitignore entriesAlways verify .env* exclusions
Using process.env in Vite client codeVite requires import.meta.env.VITE_ prefix
Hardcoding “just this one time”The hook will catch it — use env vars from the start

Complements:

  • security.md rule — broad OWASP coverage; ENV-Ninja is specifically about env var hygiene
  • nap-ninja — same pattern for business info; ENV-Ninja handles secrets

Pairs with:

  • armadillo-sync — detects scattered secrets during project setup
  • armadillo-shepherd — routes env-related requests here