beginner20 minutesevnx v0.2.0+

Prevent Secret Leaks

Set up a layered defense against secret leaks: pre-commit hooks, CI scanning, gitignore validation, and automated migration to a secret manager.

This is the guide I wish had existed before my incident. It walks through the exact layered setup I now run on every project — multiple independent checks so that if one layer fails, the next catches it.

Defense in depth

No single tool is enough. Pre-commit hooks can be bypassed with --no-verify. CI can be skipped on certain branches. This guide sets up multiple independent layers so you'd have to actively defeat each one.


The threat model

You're protecting against:

  1. Accidental commits — you typed git add . when you meant git add src/
  2. Gitignore drift — you added a new service directory but forgot to cover its .env
  3. Dependency confusion — a new developer clones the repo and commits a real .env
  4. Rushed hotfixes — 2 AM, production is down, you skip your usual process

The setup below catches all four.


Layer 1 — gitignore (foundational)

Before anything else, make sure every .env in your project is covered:

Bash
evnx doctor
[DOCTOR] Running health check...
[WARNING] packages/api/.env is tracked by git
[WARNING] .gitignore at root does not cover packages/api/.env
[OK] .gitignore covers .env at root

Fix automatically:

Bash
evnx doctor --fix

# [FIX] Added packages/api/.env to .gitignore
# [FIX] Created packages/api/.env.example from packages/api/.env

Monorepo gotcha

The most common failure mode is a monorepo where the root .gitignore covers /.env but not packages/*/env or apps/*/.env. evnx doctor recursively checks all subdirectories.

Verify nothing is tracked that shouldn't be:

Bash
git ls-files | grep '\.env$'
# If this returns any real .env files, they're tracked — remove them:
git rm --cached packages/api/.env

Layer 2 — pre-commit hook

The pre-commit hook is your primary defense. It runs before every git commit and blocks commits that contain secrets.

Setup with the pre-commit framework (recommended for teams)

Bash
# Install pre-commit
pip install pre-commit
# or: brew install pre-commit

Create .pre-commit-config.yaml in your repo root:

YAML
repos:
  - repo: local
    hooks:
      # Layer 2a: evnx secret scan
      - id: evnx-scan
        name: evnx — scan for secrets
        entry: evnx scan --exit-code --severity high
        language: system
        files: '\.env'
        pass_filenames: false
        stages: [commit]

      # Layer 2b: evnx doctor (gitignore check)
      - id: evnx-doctor
        name: evnx — environment health check
        entry: evnx doctor --exit-code
        language: system
        pass_filenames: false
        stages: [commit]

Install:

Bash
pre-commit install
# pre-commit installed at .git/hooks/pre-commit

Test it works:

Bash
# Create a file with a fake secret
echo "AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" > .env.test

git add .env.test
git commit -m "test: should be blocked"

# evnx — scan for secrets.....................................FAILED
# [ERROR] AWS_SECRET_ACCESS_KEY — high entropy string detected
# Commit blocked.

Setup for individual developers (simpler)

If you don't want to use the pre-commit framework:

Bash
cat > .git/hooks/pre-commit << 'HOOK'
#!/bin/bash
set -e

echo "evnx: scanning for secrets..."
evnx scan --exit-code --severity high

echo "evnx: checking environment health..."
evnx doctor --exit-code

echo "evnx: all checks passed"
HOOK

chmod +x .git/hooks/pre-commit

Team distribution

The .git/hooks/ directory is not committed to the repo — each developer needs to run the setup individually. The pre-commit framework approach is better for teams because .pre-commit-config.yaml is committed, and pre-commit install is a one-time setup.


Layer 3 — CI/CD gate

Pre-commit hooks can be bypassed with git commit --no-verify. CI cannot be bypassed.

Set up a GitHub Actions workflow that runs on every PR:

YAML
# .github/workflows/evnx-security.yml
name: Secret scan

on:
  pull_request:
  push:
    branches: [main]

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write

    steps:
      - uses: actions/checkout@v4

      - name: Install evnx
        run: curl -fsSL https://dotenv.space/install.sh | bash

      - name: Scan for secrets
        run: evnx scan --format github --exit-code

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: evnx.sarif
        continue-on-error: true

Set this workflow as a required status check in your branch protection rules: Settings → Branches → Branch protection rules → Require status checks.

Now git push --no-verify still gets caught at the PR level.


Layer 4 — .env.example discipline

Keep .env.example in sync with .env. When a developer clones the repo and copies .env.example to .env, they should get a complete list of required variables (with no real values).

Bash
# Generate .env.example from .env
evnx sync --generate-example

# Check if they've drifted
evnx diff .env .env.example

Add this to the pre-commit config:

YAML
- id: evnx-sync-check
  name: evnx — check .env.example is in sync
  entry: evnx diff --exit-code .env .env.example
  language: system
  files: '\.env'
  pass_filenames: false

Layer 5 — migrate to a secret manager (optional but ideal)

The ultimate fix: stop storing secrets in .env files at all. evnx migrate pushes your secrets to a cloud secret manager and replaces the values with references:

Bash
# Push to AWS Secrets Manager
evnx migrate --to aws-secrets-manager --prefix /myapp/prod

# Push to Doppler
evnx migrate --to doppler --project myapp --config prod

# Push to GitHub Actions secrets
evnx migrate --to github-actions --repo myorg/myapp

After migration, your .env contains only references:

Bash
DATABASE_URL=secret:aws:///myapp/prod/DATABASE_URL
STRIPE_SECRET_KEY=secret:aws:///myapp/prod/STRIPE_SECRET_KEY

Your application reads the actual values at runtime from the secret manager — the .env file no longer contains anything sensitive.


Verification checklist

After completing this setup, verify:

Bash
# 1. No .env files tracked by git
git ls-files | grep '\.env$'   # should return nothing

# 2. Doctor passes clean
evnx doctor

# 3. Pre-commit hook is installed
ls .git/hooks/pre-commit       # should exist and be executable

# 4. Test the hook actually blocks
echo "FAKE_KEY=AKIAIOSFODNN7EXAMPLE" >> .env
git add .env && git commit -m "test"   # should be blocked

# 5. CI workflow exists
ls .github/workflows/evnx-security.yml

Common failure modes

"I bypassed the hook with --no-verify" — That's why CI exists (Layer 3). The workflow catches what the hook misses.

"My teammate didn't install the hook" — Use the pre-commit framework and document it in your CONTRIBUTING.md. Make CI the mandatory gate.

"I have 200 existing issues in a legacy repo" — Use --exit-zero mode in CI first to audit without blocking. Fix findings progressively, then switch to --exit-code.

"The hook is too slow and developers disable it" — evnx scan runs in ~180ms. If it's slower, check --recursive isn't scanning node_modules. Use .evnx.toml to exclude large directories.


Related