Allowlisting test fixtures without blinding the scanner Jump to heading
Every secret scanner needs an allowlist, because test fixtures contain credential-shaped strings and documentation contains example keys. The question is how wide each entry is. test/** excluded entirely is one line and removes a whole directory from detection permanently β including the day someone puts a real token in a fixture to debug something and forgets. An entry scoped to one value in one file is four lines and removes nothing else. This recipe writes the narrow kind, within secret scanning and remediation.
When to use this approach Jump to heading
- Your scannerβs report is long enough that people skim it.
- Someone has proposed excluding a whole directory.
- Test fixtures and documentation examples dominate the findings.
- An existing allowlist has grown and nobody knows what it covers.
- If the report is already short and accurate, resist adding entries preemptively.
Step 1 β Classify every finding before writing a single entry Jump to heading
An allowlist written from the report without triage encodes whatever was there, including real leaks.
gitleaks detect --report-format json --report-path /tmp/leaks.json --redact
jq -r 'group_by(.File)[] | "\(length)\t\(.[0].File)"' /tmp/leaks.json | sort -rn | head -15 # Classify by hand: fixture, documented example, checksum, real, unclear
jq -r '.[] | "\(.File):\(.StartLine)\t\(.RuleID)"' /tmp/leaks.json | head -30 | nl # Verification: no finding is classified as "fixture" without someone having looked Step 2 β Prefer allowlisting the value, not the location Jump to heading
A known example credential is the same string wherever it appears, which makes it the safest thing to match.
# .gitleaks.toml
[[rules.allowlist]]
description = "AWS documentation example key β not a real credential"
regexes = ['''AKIAIOSFODNN7EXAMPLE'''] [[rules.allowlist]]
description = "RFC 7515 example JWT, used in the token parser tests"
regexes = ['''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'''] # Verification: the example is suppressed, a real-looking key is not
gitleaks detect --source /tmp/corpus --redact | grep -c 'AKIAIOSFODNN7EXAMPLE' Value-based entries survive file moves and renames, which path-based ones do not β a fixture relocated during a refactor silently starts producing findings again, and the usual response is to widen the path rather than to reconsider.
Step 3 β Where a value entry will not do, scope path and rule together Jump to heading
[[rules]]
id = "generic-api-key"
[[rules.allowlist]]
description = "Fixture tokens for the billing adapter tests; values are generated, not real"
paths = ['''^test/fixtures/billing/.*\.json$'''] # NOT this, which removes the whole directory from detection:
# [allowlist]
# paths = ['''^test/'''] # Verification: a real credential in the same directory is still caught
printf '{"token":"ghp_16C7e42F292c6912E7710c838347Ae178B4a"}\n' > test/fixtures/billing/real.json
gitleaks detect --redact | grep -c 'real.json'
git checkout -- test/fixtures/billing/ 2>/dev/null; rm -f test/fixtures/billing/real.json That verification is the point of the exercise: an entry that still catches a genuine credential in the allowlisted directory is narrow enough, and one that does not is a blind spot regardless of how it is described.
Step 4 β Record a reason for every entry Jump to heading
An allowlist without reasons cannot be audited, and an unauditable allowlist grows.
[[rules.allowlist]]
# Added 2026-09-18 by the payments team, INC-4471 follow-up.
# These are generated by the fixture builder with a fixed seed; see
# test/support/fixtures.ts. Review if the fixture format changes.
description = "Seeded fixture tokens for billing adapter tests"
paths = ['''^test/fixtures/billing/.*\.json$'''] # An entry with no description should fail review
grep -c 'description' .gitleaks.toml
grep -B2 'paths = ' .gitleaks.toml | grep -c 'description' # Verification: every allowlist block has a description
python3 - <<'PY'
import re
txt = open('.gitleaks.toml').read()
blocks = re.findall(r'\[\[rules?\.allowlist\]\](.*?)(?=\[\[|\Z)', txt, re.S)
missing = [b for b in blocks if 'description' not in b]
print(f'{len(blocks)} entries, {len(missing)} without a description')
PY SAFETY WARNING β never allowlist a value that is or was a real credential, even after rotation. The entry teaches the scanner to ignore that string permanently, and a rotated credentialβs successor frequently shares a prefix or a format. If a real value is producing repeated findings, the answer is to remove it from the working tree and decide about history, not to tell the scanner to stop mentioning it.
Step 5 β Audit the list on a schedule Jump to heading
# How many entries, and how old is each?
git log --format='%ad %h' --date=short -- .gitleaks.toml | head
git blame --date=short .gitleaks.toml | grep -E 'paths|regexes' | head # Do the paths each entry names still exist?
python3 - <<'PY'
import re, glob, os
txt = open('.gitleaks.toml').read()
for pat in re.findall(r"paths = \['''(.*?)'''\]", txt):
probe = pat.strip('^$').replace(r'.*', '*').replace(r'\.', '.')
hits = glob.glob(probe.split('*')[0] + '*')
print(('DEAD ' if not hits else 'live '), pat)
PY # Verification: dead entries are removed rather than left
grep -c 'paths = ' .gitleaks.toml Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Should the allowlist live in the repository? Jump to heading
Yes β it is a rule about this repository, it changes through review like any other rule, and keeping it elsewhere means the scanner and its configuration drift apart. The only thing that must not be in it is a real credential, which is what the warning above is about.
What about generated fixtures with no stable value? Jump to heading
Those are the genuine case for a path-and-rule entry, and the mitigation is to make the generated values obviously fake: a fixed prefix such as test_fake_ that your rules explicitly ignore is narrower than any path exclusion and survives refactors. Changing the fixture builder is often less work than maintaining the allowlist.
How do we stop the list growing? Jump to heading
Require the same justification for adding an entry as for removing a check, and audit it quarterly with the dead-path script. Most growth comes from entries added during a noisy first scan and never revisited, so a single audit after tuning removes the majority.
Related Jump to heading
- Secret Scanning & Remediation β the parent topic and keeping the signal readable.
- Choosing a Secret Scanner for Git History β reducing false positives before allowlisting them.
- Scanning Pull Requests for Secrets in CI β where a blind spot would matter most.