Skip to content

GitLeaks: Scanning Git Repositories for Secrets

Lesson 6 of 8Intermediate12 min readGit Security & DevSecOps · Secret SecurityVerified: gitleaks 8.30.1 and git 2.43.0 on Ubuntu 24.04, September 2026

GitLeaks is an open-source secret scanner. It reads Git history, working directories or standard input, matches content against a large rule set of regular expressions with entropy checks, and reports findings.

Its value in a security stack is not that it is better than GitHub secret scanning. It is that it is different: a different rule set, running at a different point, with a different failure mode. That is what makes it a layer rather than a duplicate.

Terminal window
{/* Scan the full Git history of a repository */}
gitleaks git --redact -v .
{/* Scan a directory or file on disk, ignoring Git entirely */}
gitleaks dir --redact -v .

Exit codes: 0 means no leaks, 1 means leaks or an error, 126 means an unknown flag. That makes it usable in CI without parsing output.

Always pass --redact. Without it, the report contains the secrets, and the report then becomes a file full of credentials that somebody attaches to a ticket.

The distinction matters more than it looks, because they answer different questions.

gitleaks git walks commit history using git log patches. It finds credentials that were ever committed, including ones deleted long ago. This is the mode for auditing a repository.

gitleaks dir scans files on disk with no reference to Git. It finds what is there now, including untracked and ignored files. This is the mode for a pre-commit check or for scanning something that is not a repository at all.

A repository that passes dir and fails git is the normal case for anything with history: the secret was removed from the current tree and is still in the commits.

Terminal window
gitleaks git --redact -v .

What it doesScans the repository's commit history, printing each finding with its rule, location and commit.

Why we run itHistory is where committed credentials live, and it is what everyone with a clone already has.

Expected resultA findings block per leak, then a summary line. Exit status 1 when anything is found.

Real output from a test repository containing one fake GitHub token:

Finding: github_token = "REDACTED
Secret: REDACTED
RuleID: github-pat
Entropy: 4.821928
File: secrets.cfg
Line: 1
Commit: ef62a6fa2d5d53bb80b9957019ce7eafdb356b11
Author: T
Email: t@e.com
Date: 2026-09-01T07:01:01Z
Fingerprint: ef62a6fa2d5d53bb80b9957019ce7eafdb356b11:secrets.cfg:github-pat:1
7:01AM INF 2 commits scanned.
7:01AM WRN leaks found: 1

Two fields carry most of the operational weight.

RuleID names which rule matched, which tells you whether this is a high-confidence provider pattern or a generic entropy match. github-pat is specific; a generic rule needs a human look.

Fingerprint is the stable identifier for this finding — commit:file:rule:line in git mode, and file:rule:line in dir mode. It is what allowlists and baselines match on, so it is the field you copy when you want to suppress something.

GitLeaks resolves its configuration in order: the --config/-c flag, then GITLEAKS_CONFIG, then GITLEAKS_CONFIG_TOML, then a .gitleaks.toml in the target path.

Most projects need custom rules for their own credential formats and an allowlist for known-good matches.

.gitleaks.toml
{/* Start from the built-in rules rather than replacing them */}
[extend]
useDefault = true
[[rules]]
id = "acme-service-token"
description = "Acme internal service token"
regex = '''acme_svc_[a-zA-Z0-9]{32}'''
entropy = 3.5
keywords = ["acme_svc_"]
[allowlist]
description = "Fixtures and documentation"
paths = [
'''test/fixtures/.*''',
'''docs/examples/.*''',
]

Three details that decide whether this works:

useDefault = true extends the built-in rules. Omitting [extend] replaces them, which means your one custom rule becomes the entire rule set — a configuration that finds only your tokens and none of the two hundred formats GitLeaks already knows.

keywords is a performance optimisation with a correctness consequence. GitLeaks only evaluates a rule’s regex on content containing one of its keywords. A keyword that does not appear in the credential makes the rule never fire.

Path allowlists are broad. Allowlisting test/fixtures/ means a real credential in a fixture is invisible. That is often the right trade and it should be a decision, not a default.

Three mechanisms, in increasing order of blast radius.

An inline comment on the specific line:

discord_client_secret = "8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ" #gitleaks:allow

Verified: a file whose only match carries this comment reports no leaks found. This is the narrowest suppression available and the one to prefer, because it lives next to the thing it excuses.

A .gitleaksignore file containing fingerprints, one per line. Useful when you cannot edit the matched content — a historical commit, a vendored file.

A path or regex allowlist in the configuration. The broadest, and the one that silently covers future findings you have not seen yet.

For an established repository, the first scan is the problem: hundreds of historical findings, most of them dead, arriving at once. Nobody triages that, so the scan gets removed.

A baseline solves it by recording the current state and reporting only what is new.

  1. Generate the baseline:

    Terminal window
    gitleaks git --report-path gitleaks-report.json .
  2. Scan against it:

    Terminal window
    gitleaks git --baseline-path gitleaks-report.json --report-path findings.json .

    Verified: a repository with one finding, scanned against a baseline containing it, reports no leaks found.

  3. Commit the baseline so CI uses the same one.

  4. Work the backlog separately, as a project with an owner rather than as a blocking check.

The earliest useful layer: catching a secret before there is a commit to rewrite.

.pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks

Skip it deliberately when you must:

Terminal window
SKIP=gitleaks git commit -m "message"

Two limits, both structural rather than fixable:

It is not installed by default. A fresh clone has no hooks until someone runs pre-commit install, so coverage depends on every developer’s setup.

It is skippable, by SKIP=, by --no-verify, and by committing from a tool that does not run hooks.

Neither makes it worthless — it is genuinely the cheapest place to catch a mistake. Both are why it sits in front of push protection rather than instead of it.

Two approaches, with different trade-offs.

name: gitleaks
on:
pull_request:
push:
schedule:
- cron: "0 4 * * *"
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

fetch-depth: 0 is required. A shallow clone has no history, so a history scan finds nothing and passes — the worst possible outcome, because the check is green.

More control, no licence, and you pin the version yourself:

- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install gitleaks
run: |
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" \
| tar -xz gitleaks
sudo mv gitleaks /usr/local/bin/
env:
VERSION: 8.30.1
- name: Scan history
run: gitleaks git --redact --exit-code 1 -v .

Pin the version. An unpinned installer means your CI behaviour changes when upstream releases, which for a scanner shows up as a new failing rule on an unrelated pull request.

A full history scan on every pull request is slow on a large repository and reports findings the author did not introduce. Scoping to the range under review is usually better:

- name: Scan the pull request range
run: |
gitleaks git --redact --exit-code 1 -v \
--log-opts="${BASE}..${HEAD}" .
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}

--log-opts passes arguments through to git log, so anything that selects a commit range works.

Pair it with a scheduled full scan. The pull request check keeps new secrets out; the scheduled scan catches what the range-limited check never looked at.

GitLeaks can emit SARIF, which means its findings can be uploaded to GitHub code scanning and appear in the same security tab as everything else:

- name: Scan history
run: gitleaks git --redact --exit-code 0 -v -f sarif -r gitleaks.sarif .
- name: Upload the results
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: gitleaks.sarif

Note --exit-code 0 on the scan step. The flag sets the exit code used when leaks are found, and setting it to zero lets the job continue to the upload step — otherwise a finding fails the job before its results are recorded, which is exactly backwards.

Whether you want that depends on the layer. For a pull request gate, failing is the point. For a scheduled full-history scan whose purpose is producing a triageable list, uploading is the point. Running both, in separate jobs, is the usual answer.

--report-format also accepts json, csv, junit and a custom template, which covers most other places findings need to go.

Several defaults are conservative in ways that matter.

FlagDefaultWhy it matters
--max-archive-depth0Archives are not traversed unless you opt in. A .zip in the repository is not scanned inside
--max-decode-depth5Encoded content is decoded and re-scanned, up to this depth
--max-target-megabytesunsetLarge files are scanned unless you set a limit; useful when a repository has big data files
--redactoffAccepts a percentage — --redact=20 shows part of the match, which helps triage without full disclosure
--stagedgitleaks git --staged scans only staged changes, which is the right mode for a hand-written pre-commit hook
--enable-ruleRuns only the named rules, useful for a fast targeted check
--ignore-gitleaks-allowoffIgnores #gitleaks:allow comments — worth running periodically to see what has been suppressed

That last one is a small audit worth scheduling. Inline allow comments accumulate, and nothing otherwise reports how many there are or whether they are still justified.

The archive default is the one most likely to surprise. A repository containing a .zip, a .jar or a .tar.gz is scanned as though those files were opaque blobs, because traversing archives by default would be slow and noisy. If your repository ships archives, set the depth deliberately.

They are frequently presented as alternatives. Running both is reasonable, and the reason is that their failure modes differ.

GitLeaksTruffleHog
Primary mechanismRegex rules plus entropyDetectors plus live verification
Answers“Does this look like a credential?”“Is this credential real and working?”
False positivesHigher — entropy rules catch hashes and IDsLower when filtering to verified results
False negativesFormats with no ruleProviders with no detector, or unverifiable ones
Custom formatsStraightforward TOML rulesRequires more work
SpeedFast, purely localSlower — verification makes network calls
OfflineYesVerification requires network access

The practical split many teams land on: GitLeaks locally and in the pull request gate, because it is fast, offline and easy to extend with in-house rules; TruffleHog on a schedule, because verification turns a long list into a short one and it is worth the extra time when nobody is waiting.

TruffleHog covers the other side properly.

Credentials with no distinctive shape. A password in a config file matches no provider pattern. A generic entropy rule will catch some of these and produce false positives doing it.

Encoded or split values. Base64-wrapped, or assembled from two strings at runtime.

Anything in binary or compressed content, unless you enable archive scanning explicitly.

Whether a credential is live. Every finding looks equally urgent. This is the main functional difference from TruffleHog, which verifies secrets against their providers.

Anything outside what you pointed it at. Forks, other clones and other repositories are not in scope.

Adding a scanner to a repository with history is a different exercise from adding one to a new project, and doing it in the wrong order is how the check ends up disabled.

  1. Scan without blocking anything. Run gitleaks git --redact -v . locally and read the output. Do not add it to CI yet.

  2. Triage the findings by whether they are live. Not by rule, not by age. A dead credential in a 2019 commit is cleanup; a working one is an incident. GitLeaks will not tell you which is which — that assessment is yours, and it is the reason TruffleHog’s verification is valuable here.

  3. Rotate everything live. Before anything else, and before any history rewriting. See Rotating exposed credentials.

  4. Write allowlists for the genuine false positives, narrowly — inline comments where you can edit the line, fingerprints where you cannot.

  5. Baseline the remainder, with a date and an owner recorded somewhere a person will read.

  6. Add the pull request check, scoped to the pull request’s commit range so it only reports what the author introduced.

  7. Add the scheduled full scan, non-blocking, uploading SARIF.

  8. Shrink the baseline over time. A baseline that never gets smaller is a decision nobody made.

The ordering principle is that a blocking check should be added only once it is quiet. A check that fails on its first run for reasons the author did not cause teaches everyone that this check is noise, and that lesson survives long after the noise is fixed.

Omitting --redact. The report becomes a file containing every credential in the repository, which people then attach to tickets and paste into chat.

Shallow clones in CI. No history means nothing to scan and a green check.

Replacing the default rules. Forgetting [extend] useDefault = true reduces the rule set to whatever you wrote.

A keyword that does not appear in the credential. The rule never fires, and nothing tells you.

Treating a baseline as remediation. It defers findings; the credentials in it are unchanged.

Scanning only the working tree. dir mode says nothing about history, which is where committed secrets live.

Broad path allowlists. test/ covers a real credential in a fixture just as effectively as a fake one.

Assuming it replaces push protection. It runs after a commit exists, and in CI it runs after a push. Different layer, different moment.

GitLeaks is a pattern matcher with a good default rule set and a Git-aware reader. It answers “does anything here look like a credential I have a rule for?” — not “is this credential real”, and not “is it still valid”.

  • gitleaks git scans history; gitleaks dir scans files on disk; they answer different questions
  • Exit codes are 0, 1 and 126, which makes CI integration straightforward
  • --redact keeps secrets out of the report, and should always be set
  • RuleID distinguishes a high-confidence provider match from a generic entropy match
  • Fingerprint is the stable identifier that allowlists and baselines match on
  • Well-known documentation example credentials are allowlisted by default
  • [extend] useDefault = true keeps the built-in rules; omitting it replaces them
  • A rule’s keywords gate whether its regex is evaluated at all
  • Baselines make an established repository’s scan adoptable, and defer rather than resolve
  • The GitHub Action needs a free licence key for organisation-owned repositories; the binary does not

Use a disposable repository. Generate a random value rather than copying a published example, so the default allowlist does not hide it.

  1. Create a repository. Commit a file containing a ghp_ prefix followed by 36 random alphanumeric characters.

  2. Run gitleaks dir --redact -v . Predict: what is the exit code, and what does the fingerprint look like?

  3. Run gitleaks git --redact -v . Predict: how does the fingerprint differ from step 2?

  4. Delete the file, commit, and run both again. Predict: which mode still reports the finding?

  5. Add #gitleaks:allow to a copy of the line in a new file and scan that file. Predict: does it report?

  6. Generate a baseline, then scan against it. Predict: what does it report now?

  7. Add a new secret and scan against the same baseline. Predict: is the new one reported?

  8. Try a file containing AWS’s documented example key pair. Predict: does it report? Read the result carefully before concluding the tool is broken.

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

The secrets management checklist and least-privilege token guide are in the Professional Toolkit.