cheat-sheets

Secret Scanning

Secret scanners find API keys, passwords and private keys in source code and Git history before an attacker does. This sheet covers gitleaks and TruffleHog (local and CI), pre-commit hooks, and GitHub’s built-in secret scanning with push protection. Related: Git, GitHub and the gh CLI, SOPS and age for storing secrets safely, Container Scanning for images.

Documentation: gitleaks · TruffleHog · GitHub secret scanning

If a secret was ever committed, treat it as compromised: rotate it first, then clean history. Deleting it in a later commit does not help.

gitleaks #

COMMANDDESCRIPTION
gitleaks git -vScan the full Git history of the current repo
gitleaks git --staged -vScan only staged changes (pre-commit)
gitleaks git --log-opts="--since=2025-01-01"Limit history with git log options
gitleaks dir .Scan files on disk, ignoring Git
gitleaks dir <PATH> --no-bannerScan a directory
cat file | gitleaks stdinScan piped input
gitleaks git --redactHide secret values in the output
gitleaks git -r report.json -f jsonWrite a report (json, csv, sarif, junit)
gitleaks git --baseline-path report.jsonIgnore findings already in an earlier report
gitleaks git -c .gitleaks.tomlUse a specific config
gitleaks git --exit-code 0Don’t fail the build (default is exit 1 on leaks)

Older versions (before 8.19) use gitleaks detect and gitleaks protect --staged; both still work as deprecated aliases.

.gitleaks.toml #

toml
[extend]
useDefault = true            # keep built-in rules and add yours

[[rules]]
id = "internal-api-token"
description = "Internal API token"
regex = '''myco_[A-Za-z0-9]{32}'''
keywords = ["myco_"]

[allowlist]
description = "Global allowlist"
paths = [
  '''(^|/)tests?/fixtures/''',
  '''\.lock$''',
]
regexes = ['''EXAMPLE_KEY_[A-Z0-9]+''']

Ignore one specific finding by adding its fingerprint (printed in the report) to .gitleaksignore, or add gitleaks:allow as a comment on the offending line.

TruffleHog #

COMMANDDESCRIPTION
trufflehog git file://. --only-verifiedScan local repo history, report only secrets confirmed live
trufflehog git https://github.com/<ORG>/<REPO>.gitScan a remote repo
trufflehog git file://. --since-commit <SHA> --branch HEADScan new commits only
trufflehog github --org=<ORG>Scan every repo in a GitHub org
trufflehog github --repo=https://github.com/<ORG>/<REPO>Scan one GitHub repo
trufflehog filesystem <PATH>Scan files on disk
trufflehog docker --image <IMAGE>Scan a container image
trufflehog git file://. --failExit non-zero (183) if results found
trufflehog git file://. --jsonJSON lines output
trufflehog git file://. --no-updateSkip the self-update check (CI)

--only-verified calls the provider API to check whether the credential is still valid, which means the key is sent to that provider. Drop it for an offline scan, at the cost of more false positives.

pre-commit Hooks #

Add to .pre-commit-config.yaml (install with pip install pre-commit && pre-commit install):

yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: <TAG>                 # pin to a release tag, e.g. v8.x.y
    hooks:
      - id: gitleaks
  - repo: https://github.com/trufflesecurity/trufflehog
    rev: <TAG>
    hooks:
      - id: trufflehog
        args: ["--only-verified"]
COMMANDDESCRIPTION
pre-commit run --all-filesRun every hook on the whole repo
pre-commit run gitleaks --all-filesRun one hook
pre-commit autoupdateBump rev: pins
git commit --no-verifyBypass hooks (don’t, unless it is a verified false positive)

Hooks are local and optional, so always back them with a CI check.

CI (GitHub Actions) #

yaml
name: secrets
on: [push, pull_request]
jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<SHA_OR_TAG>
        with:
          fetch-depth: 0              # full history, otherwise old leaks are missed
      - uses: gitleaks/gitleaks-action@<SHA_OR_TAG>
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The gitleaks action requires a licence key for organisation-owned repos; for those, run the binary directly instead. Pin actions to a tag or commit SHA.

GitHub Secret Scanning and Push Protection #

Free for public repos; private repos need GitHub Secret Protection (GitHub Advanced Security). Push protection blocks a push containing a recognised secret.

Enable on a repository via the API:

bash
gh api -X PATCH repos/<OWNER>/<REPO> --input - <<'EOF'
{"security_and_analysis":{
  "secret_scanning":{"status":"enabled"},
  "secret_scanning_push_protection":{"status":"enabled"}}}
EOF
COMMANDDESCRIPTION
gh api repos/<OWNER>/<REPO>/secret-scanning/alertsList alerts
gh api repos/<OWNER>/<REPO>/secret-scanning/alerts --jq '.[] | {number, secret_type, state}'Summarise alerts
gh api -X PATCH repos/<OWNER>/<REPO>/secret-scanning/alerts/<N> -f state=resolved -f resolution=revokedClose an alert after rotating

When a push is blocked, GitHub prints the file and line. Remove the secret (git commit --amend, or git rebase -i for older commits) and push again. A bypass option exists (“it’s used in tests”, “false positive”, “I’ll fix it later”) and is logged.

Removing a Leaked Secret from History #

bash
pip install git-filter-repo
git filter-repo --replace-text expressions.txt     # lines like: literal:<LEAKED_VALUE>==>REDACTED
git push --force --all && git push --force --tags

Everyone with a clone must re-clone, and forks and caches (and GitHub pull-request refs) may still hold the old objects. Rotation is the only real fix.

Troubleshooting #

SYMPTOMFIX
Too many false positivesAdd paths or regexes to the [allowlist]; use --baseline-path to adopt the tool on an existing repo
Scan misses old leaks in CIfetch-depth: 0 on checkout
gitleaks: unknown command "git"Old binary; upgrade, or use gitleaks detect
Findings print the secret in CI logsUse --redact
TruffleHog is slow on big orgsNarrow with --repo, --since-commit, or --results=verified
Search all sheets

Tip: / or Ctrl/Cmd+K to open, Esc to close.