Overview
Leaked credentials are one of the most common and most avoidable causes of breaches. An API key pasted into a config file, a .env committed by accident, a cloud token left in a test fixture: once it is pushed, it lives in git history, in forks, and in clones. Deleting the file in a later commit does not help, because the secret is still in the history and must be treated as compromised.
Gitleaks is an open-source scanner that finds secrets in git repositories, directories, and stdin using regex rules and entropy checks. In this project you will build a practical two-layer defense:
- Layer 1, pre-commit hook: scans staged changes on the developer machine and blocks the commit.
- Layer 2, CI gate: scans the full history on every push and pull request, because hooks can be skipped with
--no-verifyor never installed.
You will also learn to tune a .gitleaks.toml to cut false positives, adopt Gitleaks on a repo that already has historical findings by using a baseline, and respond when a real secret does leak.
Architecture and Design
developer machine GitHub
┌──────────────────────┐ ┌────────────────────────────┐
│ git commit │ │ push / pull_request │
│ └─ pre-commit hook │ push │ └─ Actions: gitleaks │
│ gitleaks staged ├────────▶│ full-history scan │
│ (blocks on hit) │ │ SARIF + fail the job │
└──────────────────────┘ └────────────────────────────┘
▲ ▲
└──────── shared .gitleaks.toml ────┘
Design decisions:
- One config, two enforcement points. The same
.gitleaks.tomlis used locally and in CI, so a commit that passes the hook will not surprise you in CI. - Fast locally, thorough in CI. The hook scans only staged changes (milliseconds). CI scans the whole history with
fetch-depth: 0. - Allowlist narrowly. Suppress by path, regex, or specific commit fingerprint, never by disabling rules globally.
- Redact output. Findings are printed with
--redactso CI logs do not become a second leak.
Prerequisites
- A git repository (a throwaway test repo is ideal)
- Python 3 for the
pre-commitframework, or Docker if you prefer containers - A GitHub repository with Actions enabled for the CI portion
Step 1: Install Gitleaks and Run a First Scan
Install the binary (use your package manager or download a release from GitHub):
# macOS
brew install gitleaks
# Debian/Ubuntu: download the release tarball for your arch
# https://github.com/gitleaks/gitleaks/releases
# or use the container image, no install needed:
docker run --rm -v "$PWD:/repo" ghcr.io/gitleaks/gitleaks:latest \
git /repo --redact -vScan an existing repo's full history:
gitleaks git . --redact -v --report-path /tmp/gitleaks-report.jsonNote: older guides use gitleaks detect and gitleaks protect. These still work in current releases but are deprecated aliases of gitleaks git (history, or --staged for staged changes) and gitleaks dir (plain directories). Check gitleaks --help for your installed version.
Each finding reports the rule, file, line, commit, author, and a fingerprint (commit:file:rule:line) that you will use for suppression later.
Step 2: Create a Project Config
Create .gitleaks.toml in the repo root. Extend the default ruleset so you keep every built-in detector, then add your own rules and allowlist:
title = "Project gitleaks config"
[extend]
useDefault = true
# Custom rule: internal service tokens look like svc_<32 hex chars>
[[rules]]
id = "internal-service-token"
description = "Internal service token"
regex = '''\bsvc_[a-f0-9]{32}\b'''
keywords = ["svc_"]
# Narrow allowlist for known-safe content
[allowlist]
description = "Global allowlist"
paths = [
'''(^|/)tests?/fixtures/''',
'''(^|/)docs/examples/''',
'''\.lock$''',
'''package-lock\.json$''',
]
regexes = [
'''EXAMPLE_KEY_DO_NOT_USE''',
'''AKIAIOSFODNN7EXAMPLE''',
]AKIAIOSFODNN7EXAMPLE is the documented placeholder AWS access key and is safe to allowlist. The keywords field is a speed optimization: Gitleaks only evaluates the regex on lines containing a keyword.
Step 3: Add the Pre-Commit Hook
Install the framework and create .pre-commit-config.yaml:
pip install pre-commitrepos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0 # pin to a current release tag
hooks:
- id: gitleaks
args: ["--config=.gitleaks.toml", "--redact"]Activate it:
pre-commit install
pre-commit autoupdate # bumps rev to the latest tag
pre-commit run --all-filesThe hook now runs on every git commit. Commit both files so the whole team shares the rules. Pin rev to a tag rather than main so a surprise upstream change cannot break your commits, and let Renovate or pre-commit autoupdate handle bumps.
Step 4: Add the CI Gate
Create .github/workflows/secret-scan.yml:
name: Secret Scan
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
security-events: write # for SARIF upload
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history, required for a real scan
- name: Install gitleaks
run: |
VER=8.30.0
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" \
| tar -xz gitleaks
sudo mv gitleaks /usr/local/bin/
- name: Scan history
run: |
gitleaks git . \
--config .gitleaks.toml \
--redact -v \
--report-format sarif \
--report-path gitleaks.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gitleaks.sarifRunning the binary directly avoids the license requirement that the packaged gitleaks-action imposes on organization-owned repos, and it keeps the exact same command as your local run. Gitleaks exits with code 1 on findings, which fails the job. Mark this check as required under branch protection so it cannot be merged around.
Step 5: Adopt on a Repo with Existing Findings
A legacy repo will light up with historical findings, and blocking on all of them makes the tool unusable. Use a baseline: record today's findings and only fail on new ones.
gitleaks git . --report-format json --report-path .gitleaks-baseline.json
git add .gitleaks-baseline.jsonThen run with the baseline in both the hook args and CI:
gitleaks git . --config .gitleaks.toml --baseline-path .gitleaks-baseline.json --redactImportant caveat: a baseline suppresses findings, it does not fix them. Every secret in the baseline that was real must still be rotated. Track that as a separate task and shrink the baseline over time.
For one-off false positives, add the fingerprint to a .gitleaksignore file in the repo root, one per line, ideally with a comment:
# Test token used in unit tests, not valid anywhere
3f9a1c2d...:tests/auth.test.ts:generic-api-key:42An inline # gitleaks:allow comment on the offending line works for single lines in code.
Testing
Prove each layer works with a harmless fake secret. Use a throwaway branch.
git checkout -b test/secret-scan
echo 'github_token = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"' > leak.py
git add leak.py
git commit -m "test: leak"Expected: the hook fails, prints the rule (for example github-pat), the file and line, and the secret as REDACTED. The commit is not created.
Now bypass the hook to simulate a developer who skipped it, then push:
git commit --no-verify -m "test: leak (bypass)"
git push -u origin test/secret-scanOpen a pull request. The Secret Scan job should fail and the findings should appear in the Security tab via SARIF. Also test your custom rule with a svc_ token of 32 hex characters, and verify an allowlisted path such as tests/fixtures/ does not trigger. Delete the test branch afterwards; if you pushed it, remember the fake value is in that history.
Deployment
Roll this out across many repos without hand-copying files:
- Template repo or org-level reusable workflow. Put the workflow in a central
.githubrepo and call it withuses: org/.github/.github/workflows/secret-scan.yml@main. - Shared config. Keep a canonical
.gitleaks.tomlin the central repo and have the workflow fetch it, or use the[extend] path =option so local files add to a base. - Bootstrap script for developers:
#!/usr/bin/env bash
set -euo pipefail
pip install --user pre-commit
pre-commit install
pre-commit run --all-files- Add GitHub push protection as a third layer. It blocks known provider token patterns server-side at push time and is the only layer a developer cannot bypass locally. Gitleaks complements it by catching custom and generic secrets that provider patterns do not know about.
- Keep it pinned and updated. Use Renovate to bump the hook
revand the CI version together.
Incident Response: When a Real Secret Leaks
If CI flags a genuine credential:
- Revoke and rotate immediately. This is the only step that actually fixes the exposure. Assume it was already harvested.
- Check the provider's audit log for use of the key between commit time and rotation.
- Optionally rewrite history with
git filter-repoto remove it, but only after rotating. Forks and caches may still hold it. - Add a rule or allowlist change only if the detection exposed a gap, not to silence a real finding.
Extensions and Next Steps
- Add TruffleHog in CI with verification enabled, which tests whether detected credentials are still live and cuts triage time.
- Scan container images and build contexts with
gitleaks dirbefore publishing. - Feed SARIF or JSON findings into DefectDojo for tracking and metrics.
- Write more custom rules for your own token formats and measure false-positive rate monthly.
- Replace long-lived secrets with short-lived credentials (OIDC federation for CI, a secrets manager such as Vault) so a leak has a small blast radius.
- Add a scheduled weekly full-history scan to catch rules updated after a commit landed.