Pre-commit hook to block committed secrets

The cheapest secret leak to prevent is the one a pre-commit hook catches before git commit finishes. The catch: a local hook can be bypassed, so it needs a CI backstop. This page sets up both, extending CI/CD Config Validation.

Problem 1: relying on memory

# ANTI-PATTERN: "I'll remember not to commit .env" — until you don't
# git add . && git commit -m "wip"   # .env with live keys is now in history

A single git add . is all it takes; rotating the leaked credential is the only fix once it is in history.

Problem 2: a hook with no CI backstop

# ANTI-PATTERN: local hook only — trivially skipped
git commit --no-verify     # bypasses every local pre-commit hook

Local hooks are advisory; --no-verify skips them. CI must enforce the same scan.

Secure implementation

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks                 # scans staged changes for secrets
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ["--baseline", ".secrets.baseline"]
# one-time setup
pip install pre-commit detect-secrets
detect-secrets scan > .secrets.baseline   # record known-safe matches
pre-commit install                        # activate the local hook
# .github/workflows/secrets.yml — the backstop that cannot be skipped
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: gitleaks detect --no-banner   # fails the build on any finding

The local hook gives instant feedback; the CI job is the gate that --no-verify cannot bypass. The baseline records intentional matches (test fixtures) so they do not block every commit.

Gotchas & version-specific behaviour

  • A leaked secret must be rotated, not just removed — history is forever.
  • detect-secrets needs a baseline; regenerate it when adding legitimate fixtures.
  • gitleaks detect with fetch-depth: 0 scans full history in CI; the pre-commit hook scans staged changes only.
  • Keep .env in .gitignore as the first line of defence; the scanner is the second.

Production parity checklist

  • pre-commit install is run by every developer (document it in the README).
  • The same scanner runs in CI with full history.
  • .env and *.pem are gitignored.
  • A .secrets.baseline is committed and kept current.
  • Any real hit triggers immediate credential rotation.

Conclusion

Pair a local pre-commit scanner with a CI job that cannot be skipped, and committed secrets are caught before they become permanent. This is the local half of CI/CD Config Validation.