Where secrets hide in a codebase

The most common places developers accidentally commit credentials:

  • Configuration files: .env, config.yml, settings.py, application.properties β€” committed without realising they contain real credentials.
  • Test files: Developers hardcode real credentials in tests for convenience, intending to remove them later. They never do.
  • Commented-out code: // password = "old_prod_password" β€” removed from active code but still in the file.
  • Git history: A secret committed, then deleted in the next commit. The deletion doesn't remove it from history.
  • CI configuration: .travis.yml, Jenkinsfile, GitHub Actions workflow files with secrets inlined (not using encrypted secrets).
  • Jupyter notebooks: .ipynb files with API calls in cells that include tokens in the request examples.

The rotation trap: Finding a secret in your repo and rotating it is not enough. The old secret still exists in every git clone, every CI cache, and every GitHub fork ever made of the repository. If the repo was ever public, assume the old secret is already in attacker tooling.

Quick grep scan for common secret patterns

Terminal β€” secret pattern grepshell
# Common API key and token patterns
$ grep -rEin "api[_-]?key\s*[=:]\s*['\"][a-z0-9]{20,}" .
$ grep -rEin "password\s*[=:]\s*['\"][^'\"]{8,}" .
$ grep -rEin "secret[_-]?key\s*[=:]\s*['\"]" .
$ grep -rEin "access[_-]?token\s*[=:]\s*['\"]" .

# AWS credentials
$ grep -rEn "AKIA[0-9A-Z]{16}" .
$ grep -rEn "aws[_-]?secret" -i .

# Common service-specific patterns
$ grep -rEn "sk_live_[0-9a-zA-Z]{24}" .       # Stripe live key
$ grep -rEn "ghp_[0-9a-zA-Z]{36}" .           # GitHub PAT
$ grep -rEn "xoxb-[0-9]+-[0-9a-zA-Z]+" .     # Slack bot token
$ grep -rEn "AIza[0-9A-Za-z_-]{35}" .         # Google API key

# Also check .env files specifically
$ find . -name "*.env" -o -name ".env*" | xargs grep -l "." 2>/dev/null

Trufflehog β€” scan the entire git history

Trufflehog is the most thorough secrets scanner for Git repositories. It scans every commit in the full git history, detects secrets using both regex patterns and entropy analysis, and validates findings against real APIs to confirm they are active (not rotated or invalid).

Trufflehog β€” full repo scanshell
# Install
$ brew install trufflehog
# or: curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh

# Scan local git repo (entire history)
$ trufflehog git file://. --only-verified

# Scan a remote GitHub repo
$ trufflehog github --repo https://github.com/yourorg/yourrepo --only-verified

# Scan only recent commits (faster for CI)
$ trufflehog git file://. --since-commit HEAD~50 --only-verified

# Output as JSON for automation
$ trufflehog git file://. --json 2>&1 | jq '.SourceMetadata'

The --only-verified flag makes Trufflehog check each found credential against the respective API (AWS, GitHub, Stripe, etc.) to confirm it's currently active. This dramatically reduces false positives β€” you only see secrets that are real and working.

Gitleaks β€” fast regex-based scanning

Gitleaks is faster than Trufflehog (no API verification) and better suited for CI pipeline gates where speed matters. It ships with 150+ pre-built rules covering all major secret types.

Gitleaks β€” install and scanshell
# Install
$ brew install gitleaks

# Scan entire repo history
$ gitleaks detect --source . --verbose

# Scan only staged files (pre-commit use)
$ gitleaks protect --staged --verbose

# Generate a report
$ gitleaks detect --source . --report-path gitleaks-report.json

# Exit code 1 if secrets found β†’ blocks CI
$ gitleaks detect --source . --exit-code 1

Pre-commit hook: block secrets before they're committed

The cheapest place to catch a secret is before it enters git history. A pre-commit hook runs on every git commit on the developer's machine.

.pre-commit-config.yamlyaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.0
    hooks:
      - id: gitleaks

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']
Install pre-commit hooksshell
$ pip install pre-commit
$ pre-commit install
$ pre-commit run --all-files  # run against existing codebase first

CI/CD gate: catch anything that slips through pre-commit

.github/workflows/secrets-scan.ymlyaml
name: Secrets Scan
on: [push, pull_request]

jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
        with:
          fetch-depth: 0  # full history for git log scan

      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{{ secrets.GITHUB_TOKEN }}}
          GITLEAKS_LICENSE: ${{{ secrets.GITLEAKS_LICENSE }}}

What to do after finding a secret in your repository

  1. Rotate the credential immediately. Disable or revoke the compromised key/token/password before doing anything else. Don't wait until the cleanup is complete.
  2. Assume it has been used. Check the service's audit logs (AWS CloudTrail, GitHub audit log, Stripe logs) for any suspicious activity using that credential.
  3. Remove from current code and replace with an environment variable reference or secrets manager reference.
  4. Do NOT rewrite git history if the repo is shared β€” history rewrites force all collaborators to reset their local clones and can break CI. The rotated credential is worthless anyway.
  5. If the repo was ever public: Treat the credential as permanently compromised regardless of whether you can see it in the current default branch. GitHub forks and clones may have captured it.

Never try to hide the commit by force-pushing or rebasing. If the repository is public, GitHub retains objects for a period even after force-push. Rotation is the only reliable remediation.

Scan your codebase for hardcoded secrets automatically

AquilaX detects 800+ secret types across your entire codebase and git history β€” with verified-secret detection and automated CI blocking.

See secrets scanning β†’