Catch secrets — API keys, tokens, private keys — before they ever leave a laptop, and find the ones already buried in your Git history. You'll scan a repo and its full history, block commits with a pre-commit hook, write custom rules, baseline existing findings, gate CI, and run the correct response when a real key leaks. Hands-on throughout.
The journey:
| You need | Why |
|---|---|
The gitleaks CLI |
Scan repos, history, and staged changes |
| A terminal | Everything starts on the CLI |
| A Git repo | GitLeaks understands Git internals |
Install with brew install gitleaks, the release binary, or the Docker image. Confirm: gitleaks version.
GitLeaks scans code and Git history for hardcoded secrets using a large set of regex rules plus entropy checks (random-looking high-entropy strings are often keys). It's fast, dependency-free, and designed to run both as a local hook and a CI gate.
Secrets are different from other findings in one crucial way: once a secret hits a shared remote, it is compromised — forks, clones, CI logs, and mirrors may already have it. So the entire strategy is to stop them before they're pushed, and to treat any that slip through as already-public.
The default command scans the entire Git history of the current repo:
# Scan all commits in history
gitleaks detect --source . --verbose
# Scan a directory's files without Git (no history, just the tree)
gitleaks detect --source . --no-git
# Write a report
gitleaks detect --source . --report-format json --report-path leaks.json
Each finding shows the rule that matched, the file, the commit, the author, and the line — so you can see exactly when and by whom a secret was introduced.
GitLeaks has two modes, and knowing which to use where is the whole game:
# detect: scan committed history (use in CI and audits)
gitleaks detect --source .
# protect: scan uncommitted, staged changes (use in a pre-commit hook)
gitleaks protect --staged --source .
detect looks at what's already committed — the right choice for CI and periodic audits.protect --staged looks at what you're about to commit — the right choice for a pre-commit hook, because it stops the secret before it enters history at all.The highest-value use of GitLeaks is the pre-commit hook — it prevents the secret from ever being created as a commit. With the pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
pre-commit install # activate the hook
# now every `git commit` runs `gitleaks protect --staged` and blocks on a match
A leak caught here costs nothing to fix — you just don't commit it. A leak caught in CI means it already reached a remote and the key must be rotated. That difference is why the hook matters.
GitLeaks ships strong defaults, but you'll want rules for your own internal token formats. Config is TOML:
# .gitleaks.toml
title = "org gitleaks config"
[extend]
useDefault = true # keep all built-in rules, then add your own
[[rules]]
id = "internal-service-token"
description = "Internal service token (acme_...)"
regex = '''acme_[a-zA-Z0-9]{32}'''
keywords = ["acme_"] # keywords pre-filter for speed
gitleaks detect --source . --config .gitleaks.toml
keywords narrow the search before the regex runs, which keeps large scans fast. [extend] useDefault = true keeps the built-in ruleset while you layer org-specific rules on top.
Turning GitLeaks on for an old repo surfaces every secret ever committed. Baseline so you only block new leaks:
# 1. Capture current findings as a baseline
gitleaks detect --source . --report-path baseline.json
# 2. Future scans ignore anything already in the baseline
gitleaks detect --source . --baseline-path baseline.json
For known-safe matches (test fixtures, example keys, sample data), allowlist them in config:
[allowlist]
description = "known safe test fixtures"
paths = ['''tests/fixtures/.*''']
regexes = ['''EXAMPLE_KEY_[0-9]+''']
Note baselining an already-committed real secret does not make it safe — it's still in history and must be rotated. Baseline is for triage ordering, not absolution.
CI is the backstop for anything the hook missed (not everyone installs hooks):
# .github/workflows/gitleaks.yml
name: gitleaks
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so detect sees all commits
- uses: gitleaks/gitleaks-action@v2
env:
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # org use only
fetch-depth: 0 matters — a shallow clone hides most of history, so the scan would miss older commits. GitLeaks exits non-zero on a finding, failing the build.
This is the question every interview asks, and the order is what people get wrong:
git filter-repo or BFG to remove the secret from past commits — but only after rotation, and knowing it won't help if the repo was already cloned.# remove a file's secret from all history (after rotating!)
git filter-repo --path config/secrets.env --invert-paths
fetch-depth: 0 so history is fully scanned.[extend] useDefault = true plus org-specific rules for your own token formats.fetch-depth: 0, detect misses most of history.gitleaks detect --source . --verbose on a repo and read a finding's commit/author.Self-check:
detect vs protect --staged?fetch-depth: 0 be set for CI scans?You now have the loop: pre-commit protect → CI detect → custom rules → baseline → rotate-first response. That's GitLeaks keeping secrets out of your history for good.