Choosing a secret scanner for git history Jump to heading

Every scanner claims high accuracy, and the number that matters is its false-positive rate on your repository rather than on a benchmark. A tool that flags every high-entropy string finds all your credentials and a thousand hashes, test fixtures and base64 blobs; one that only matches known provider formats produces a readable report and misses your internal token scheme entirely. The only way to choose is to run the candidates over the same history and count. This recipe does that, within secret scanning and remediation.

When to use this approach Jump to heading

  • You are introducing scanning and have not picked a tool.
  • An existing scanner produces too much noise to be read.
  • A leak got through and you want to know whether detection would have caught it.
  • You need scanning in several places and want one tool for all of them.
  • If your provider’s built-in scanning already covers your credential types, start there and add a tool only for what it misses.

Step 1 — Assemble a realistic test corpus Jump to heading

Benchmarks do not predict behaviour on your code. Your own history does.

# A known-leak corpus: credentials you have already rotated, from real commits
git log --all --oneline | wc -l
# Plus deliberately planted samples of the formats you care about
mkdir -p /tmp/corpus && cd /tmp/corpus && git init -q
printf 'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' > a.env
printf 'internal_token=int_live_9f8a7b6c5d4e3f2a1b0c\n' > b.env
printf 'sha256=4f2a1b3c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a\n' > c.txt
git add -A && git commit -qm 'corpus'
# Verification: the corpus contains both true positives and likely false ones
ls /tmp/corpus

The third file matters as much as the first two: a scanner that flags a checksum as a secret will flag thousands of them across a real repository.

Step 2 — Run each candidate over the same history Jump to heading

gitleaks detect --source . --report-format json --report-path /tmp/gitleaks.json --redact
jq 'length' /tmp/gitleaks.json
trufflehog git file://. --json --only-verified > /tmp/trufflehog.jsonl 2>/dev/null
wc -l < /tmp/trufflehog.jsonl
# The provider's own scanning, where available
gh api repos/:owner/:repo/secret-scanning/alerts --jq 'length' 2>/dev/null || echo "not enabled"
# Verification: all three ran against the same commits
git rev-list --count --all
Two detection philosophiesPattern-based scanners match known credential formats, producing few false positives and missing internal schemes. Entropy-based scanners flag anything that looks random, catching everything and burying it in checksums and test data.Entropy-basedPattern-basedfinds unknown formatsyesnofalse positivesmanyfewreport readableafter heavy tuningimmediatelyverifies validityrarelysome tools domost teams end with a pattern-based default plus entropy rules for their own token formats

Step 3 — Count true and false positives honestly Jump to heading

This is the step that decides the answer, and it takes an hour.

# Extract findings to a reviewable list
jq -r '.[] | "\(.File):\(.StartLine)\t\(.RuleID)"' /tmp/gitleaks.json | sort -u > /tmp/g.txt
wc -l /tmp/g.txt
# Classify a sample by hand: real, fixture, checksum, example, unclear
head -40 /tmp/g.txt | nl
# Verification: compute the rate from the classified sample
# false positives / total, over the sample
Findings per thousand commits, by toolOn the same repository, an entropy-based scanner reported several times more findings than a pattern-based one, and almost all of the difference was checksums and test fixtures. Both found the same four real credentials.findings per 1000 commitsentropy-based, untuned340pattern-based46pattern-based, verified only9actual live credentials4the bottom two bars are close, which is what makes verification worth having

Tools that verify a candidate against the issuing service — attempting an authenticated call and reporting only what succeeds — collapse the noise dramatically. The cost is that they make network requests during scanning, which has its own implications in CI.

Step 4 — Decide what runs where Jump to heading

One tool rarely fits all three layers, and that is fine.

# Pre-commit: fast, local, no network
gitleaks protect --staged --redact
# Pull request: the range, covering forks
      - run: gitleaks detect --log-opts="$(git merge-base origin/main HEAD)..HEAD" --redact
# Scheduled: full history, verification enabled, network permitted
trufflehog git file://. --only-verified --json > /tmp/verified.jsonl
# Verification: each layer runs and reports separately
gitleaks version; trufflehog --version 2>&1 | head -1

SAFETY WARNING — a verifying scanner sends candidate credentials to the services that issued them in order to test validity. That is what makes it accurate and it means your CI runner makes authenticated requests with values from your repository, which may be logged by the provider and may trigger alerts on their side. Understand that before enabling verification, and never enable it on a runner you do not control.

Step 5 — Tune, then make it blocking Jump to heading

A scanner that blocks before it is tuned gets disabled within a week.

# Start in report-only mode, for a fortnight
gitleaks detect --report-path /tmp/report.json --exit-code 0
# .gitleaks.toml — narrow allowlists, each with a reason
[[rules.allowlist]]
description = "Documented example key from the AWS documentation"
paths = ['''^docs/examples/aws\.md$''']
regexes = ['''AKIAIOSFODNN7EXAMPLE''']
# Then enable blocking, once the report is short enough to read
gitleaks detect --exit-code 1
# Verification: a real credential still fails, a fixture does not
gitleaks detect --source /tmp/corpus --exit-code 1 | tail -3
From evaluation to a blocking gateBuild a corpus from your own history, run the candidates, count false positives by hand, choose per layer, run in report-only mode while tuning, and enable blocking once the report is short enough that people read it.Corpusyour historyplanted samplesRun candidatessame commitsCount by handtrue vs falseTunereport-onlyBlockonce readableskipping the fourth box is the most common reason scanning is abandoned

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Is the provider’s built-in scanning enough? Jump to heading

It covers the credential formats its partners have registered, which is a large and growing set, and it has one advantage no tool can match: partner notification, where the provider tells the issuer to revoke a leaked key automatically. It will not know about your internal token formats, which is where an additional tool earns its place.

How often should the full-history scan run? Jump to heading

Weekly or monthly is enough, because its purpose is to catch what new detection rules now recognise rather than to find today’s mistakes — the pre-commit and pull request layers handle those. Running it unattended on a schedule means the cost is zero and the coverage improves as rules improve.

Does scanning slow down commits noticeably? Jump to heading

A staged-content scan is milliseconds, because it looks at the diff rather than the repository. A full-history scan is minutes on a large repository, which is why it belongs in a scheduled job and not in a hook — the layering in Step 4 exists precisely to keep the interactive path fast.