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.
The short answer
Section titled “The short answer”{/* 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 two scan modes
Section titled “The two scan modes”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.
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 = "REDACTEDSecret: REDACTEDRuleID: github-patEntropy: 4.821928File: secrets.cfgLine: 1Commit: ef62a6fa2d5d53bb80b9957019ce7eafdb356b11Author: TEmail: t@e.comDate: 2026-09-01T07:01:01ZFingerprint: ef62a6fa2d5d53bb80b9957019ce7eafdb356b11:secrets.cfg:github-pat:1
7:01AM INF 2 commits scanned.7:01AM WRN leaks found: 1Two 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.
Configuration
Section titled “Configuration”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.
{/* 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.5keywords = ["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.
Suppressing individual findings
Section titled “Suppressing individual findings”Three mechanisms, in increasing order of blast radius.
An inline comment on the specific line:
discord_client_secret = "8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ" #gitleaks:allowVerified: 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.
Baselines
Section titled “Baselines”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.
-
Generate the baseline:
Terminal window gitleaks git --report-path gitleaks-report.json . -
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. -
Commit the baseline so CI uses the same one.
-
Work the backlog separately, as a project with an owner rather than as a blocking check.
Pre-commit integration
Section titled “Pre-commit integration”The earliest useful layer: catching a secret before there is a commit to rewrite.
repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.30.1 hooks: - id: gitleaksSkip it deliberately when you must:
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.
CI integration
Section titled “CI integration”Two approaches, with different trade-offs.
The official action
Section titled “The official action”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.
Running the binary directly
Section titled “Running the binary directly”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.
Scanning only the pull request
Section titled “Scanning only the 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.
Reporting into GitHub’s alert interface
Section titled “Reporting into GitHub’s alert interface”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.sarifNote --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.
Flags worth knowing
Section titled “Flags worth knowing”Several defaults are conservative in ways that matter.
| Flag | Default | Why it matters |
|---|---|---|
--max-archive-depth | 0 | Archives are not traversed unless you opt in. A .zip in the repository is not scanned inside |
--max-decode-depth | 5 | Encoded content is decoded and re-scanned, up to this depth |
--max-target-megabytes | unset | Large files are scanned unless you set a limit; useful when a repository has big data files |
--redact | off | Accepts a percentage — --redact=20 shows part of the match, which helps triage without full disclosure |
--staged | — | gitleaks git --staged scans only staged changes, which is the right mode for a hand-written pre-commit hook |
--enable-rule | — | Runs only the named rules, useful for a fast targeted check |
--ignore-gitleaks-allow | off | Ignores #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.
GitLeaks and TruffleHog together
Section titled “GitLeaks and TruffleHog together”They are frequently presented as alternatives. Running both is reasonable, and the reason is that their failure modes differ.
| GitLeaks | TruffleHog | |
|---|---|---|
| Primary mechanism | Regex rules plus entropy | Detectors plus live verification |
| Answers | “Does this look like a credential?” | “Is this credential real and working?” |
| False positives | Higher — entropy rules catch hashes and IDs | Lower when filtering to verified results |
| False negatives | Formats with no rule | Providers with no detector, or unverifiable ones |
| Custom formats | Straightforward TOML rules | Requires more work |
| Speed | Fast, purely local | Slower — verification makes network calls |
| Offline | Yes | Verification 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.
What GitLeaks does not find
Section titled “What GitLeaks does not find”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.
Rolling it out on an existing repository
Section titled “Rolling it out on an existing repository”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.
-
Scan without blocking anything. Run
gitleaks git --redact -v .locally and read the output. Do not add it to CI yet. -
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.
-
Rotate everything live. Before anything else, and before any history rewriting. See Rotating exposed credentials.
-
Write allowlists for the genuine false positives, narrowly — inline comments where you can edit the line, fingerprints where you cannot.
-
Baseline the remainder, with a date and an owner recorded somewhere a person will read.
-
Add the pull request check, scoped to the pull request’s commit range so it only reports what the author introduced.
-
Add the scheduled full scan, non-blocking, uploading SARIF.
-
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.
Common mistakes
Section titled “Common mistakes”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.
Mental model
Section titled “Mental model”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”.
What you learned
Section titled “What you learned”gitleaks gitscans history;gitleaks dirscans files on disk; they answer different questions- Exit codes are
0,1and126, which makes CI integration straightforward --redactkeeps secrets out of the report, and should always be setRuleIDdistinguishes a high-confidence provider match from a generic entropy matchFingerprintis the stable identifier that allowlists and baselines match on- Well-known documentation example credentials are allowlisted by default
[extend] useDefault = truekeeps the built-in rules; omitting it replaces them- A rule’s
keywordsgate 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
Exercise
Section titled “Exercise”Use a disposable repository. Generate a random value rather than copying a published example, so the default allowlist does not hide it.
-
Create a repository. Commit a file containing a
ghp_prefix followed by 36 random alphanumeric characters. -
Run
gitleaks dir --redact -v .Predict: what is the exit code, and what does the fingerprint look like? -
Run
gitleaks git --redact -v .Predict: how does the fingerprint differ from step 2? -
Delete the file, commit, and run both again. Predict: which mode still reports the finding?
-
Add
#gitleaks:allowto a copy of the line in a new file and scan that file. Predict: does it report? -
Generate a baseline, then scan against it. Predict: what does it report now?
-
Add a new secret and scan against the same baseline. Predict: is the new one reported?
-
Try a file containing AWS’s documented example key pair. Predict: does it report? Read the result carefully before concluding the tool is broken.
Related lessons
Section titled “Related lessons”The secrets management checklist and least-privilege token guide are in the Professional Toolkit.