A single AWS key sitting in a public commit is still the fastest way for an attacker to skip past every firewall a company owns. GitGuardian’s State of Secrets Sprawl 2026 report puts a number on how often that happens: 28.6 million new secrets turned up in public GitHub commits in 2025 alone, a 34% jump year over year, and 64% of secrets discovered in historical commits four years ago are still live and exploitable today. Nobody rotated them. This tutorial walks through setting up Gitleaks, the open-source scanner with 29,000 GitHub stars that most engineering teams now run as a baseline check, and pairs it with TruffleHog for live credential verification. By the end you will have a working pipeline that blocks a commit, a pull request, and a merge the moment a real API key, database password, or private certificate shows up in a diff.
The timing matters. The ChainDrop npm worm that hit 444 packages and an estimated 2 billion downloads earlier this year spread in part because compromised maintainer tokens were sitting in plaintext where an automated scanner should have caught them first. Secret scanning will not stop a determined attacker on its own, but it closes the cheapest, most common entry point: a developer who pasted a credential into a config file, committed it, and forgot. That same pattern shows up across most of the supply chain attacks reshaping cyber risk management this year, where the initial foothold traces back to a stray credential rather than a novel exploit.
By the time you finish this tutorial, you will have installed Gitleaks and TruffleHog, run a first scan against a real repository, written a custom ruleset, wired both tools into a pre-commit hook and two CI platforms, and set up a scheduled audit that catches anything the per-commit checks miss. Every command below has been tested against the current stable release of each tool, and every version number cited comes from the official release pages rather than a guess. None of the steps require a paid product, a vendor sales call, or elevated infrastructure access beyond what you already have as a repository maintainer, which is part of why this pairing has become the default starting point rather than a heavier commercial secrets-management suite.
Why Leaked Secrets in Git Repos Keep Getting Worse in 2026
Git was never built to hide anything. Every commit stays in the object database forever unless someone deliberately rewrites history, which means a secret pasted into a config file on day one of a project is still retrievable on day one thousand, even after the line gets deleted in a later commit. AI-assisted coding has made the problem sharper rather than smaller. GitGuardian’s 2026 data shows commits assisted by tools like Cursor, Claude Code, and Lovable now account for a measurable share of new secret leaks, largely because generated boilerplate often ships with placeholder credentials that developers forget to swap out before pushing.
The same report found that 44% of compromised developer machines held more than 10 live secrets, and 5% carried over 100. Nearly a third of secrets sprawl, 28% by GitGuardian’s count, happens entirely outside code repositories, in Slack exports, CI logs, and internal wikis, but the repository is still where most automated tooling starts because it is the one place a scanner can attach to a pull request and block a merge before damage happens. That is the scope of this guide: stopping the leak at the point where a fix is still cheap, before a key reaches a public mirror or a bot starts scraping it.
The cost gap between catching a leak at commit time and catching it after exposure is not small. A credential caught by a pre-commit hook costs a developer a few seconds and a deleted line. The same credential caught three weeks later, after it has been indexed by a scraper and possibly used, costs an incident response cycle, a forced rotation across every service that shared the key, and in regulated industries a disclosure review. Automated scanning does not eliminate that gap entirely, but it collapses the average time-to-discovery from weeks to minutes, which is the only lever that actually changes the outcome.
What Gitleaks Detects, and Where TruffleHog Fits In
Gitleaks is a static pattern matcher. It ships with a regularly updated default ruleset covering roughly 180 secret types, AWS access keys, Stripe tokens, Slack webhooks, private key headers, generic high-entropy strings, and scans a repository’s working tree or full commit history for anything matching those patterns. It is fast (a single Go binary with no runtime dependencies), free under an MIT license, and has become the de facto default for pre-commit hooks and CI pipelines. The tradeoff is that pattern matching cannot tell you whether a flagged string is a live, working credential or a dead test fixture from three years ago.
TruffleHog closes that gap. Beyond pattern detection, it makes live API calls against the service each credential type belongs to (checking an AWS key against STS, a Slack token against its auth endpoint, and so on) and reports back whether the secret is currently valid. Run with the --only-verified flag, TruffleHog turns a report with 400 possible matches into one with the handful that actually need a rotation call today. The two tools are not competitors. Most mature setups run Gitleaks on every commit for speed and coverage, then run TruffleHog on a schedule or before a release to separate signal from noise.
| Capability | Gitleaks v8.30.1 | TruffleHog v3.97.1 |
|---|---|---|
| Detection method | Regex and entropy pattern matching | Pattern matching plus live credential verification |
| Confirms if a secret is live | No | Yes, via API calls to the issuing service |
| Typical scan speed on a mid-size repo | Seconds | Slower, due to verification API calls |
| License | MIT (free, open source) | AGPL-3.0 core (free), paid Enterprise tier available |
| GitHub stars | 29,028 | 27,636 |
| Best fit in a pipeline | Pre-commit hook and every CI run | Scheduled deep scan or pre-release gate |
Which Secret Types Each Tool Catches Best
Not every credential format is equally easy to detect. Named formats with a fixed structure, an AWS access key always starts with AKIA followed by 16 characters, a Stripe live key always starts with sk_live_, are the easiest wins for either tool, because a regex against a known prefix rarely misses and rarely false-positives. The harder category is anything without a distinctive shape: a raw database password, an internal JWT signing secret, or a plain 32-character API token with no vendor prefix. Those rely on entropy scoring, measuring how random a string looks, which is inherently noisier and is where most of the tuning work in a mature setup actually goes.
| Secret category | Example | Detection reliability |
|---|---|---|
| Named vendor token (fixed prefix) | AWS access key, Stripe secret key, Slack webhook URL | High, low false-positive rate |
| Structured but vendor-agnostic | PEM-format private key headers, JWTs | High for the format, but validity still needs verification |
| Generic high-entropy string | Raw database password, internal service token | Moderate, prone to false positives on test data and hashes |
| Low-entropy or short secrets | Short shared passwords, PINs, simple internal codes | Low, easy to miss without a custom rule |
That last row is worth planning for explicitly. A short, low-entropy internal password will not trip a generic entropy rule, since it does not look random enough to stand out from ordinary code. The only reliable way to catch it is the custom rule shown in Step 4, written specifically for your organization’s naming conventions rather than relying on the community default ruleset to guess a pattern it has never seen.
Prerequisites and Versions You’ll Need
- Git 2.40 or later installed and a repository you have permission to scan
- Gitleaks v8.30.1 (the current stable release as of this writing, released March 21, 2026)
- TruffleHog v3.97.1 (released August 24, 2026)
- Docker 24 or later, optional, for containerized scanning without a local binary
- Python 3.9+ with pip, needed only for the git-filter-repo history rewrite step
- A GitHub or GitLab account with Actions or CI/CD pipelines enabled on the target repository
- The pre-commit framework (installed via pip) if you want local hook enforcement
- Administrator or maintainer access to the repository to add branch protection rules later
None of this requires a paid license. Gitleaks and TruffleHog’s open-source cores are both free for unlimited scanning. The only place licensing enters the picture is the official gitleaks-action for GitHub Actions, which requires a Gitleaks license key for organization-owned repositories (personal accounts run it free). This guide uses the open-source CLI directly in CI, which sidesteps that requirement entirely.
Step 1: Install Gitleaks
Three install paths cover almost every setup. Pick whichever matches your environment.
# macOS or Linux via Homebrew
brew install gitleaks
# Go toolchain (any OS with Go 1.21+)
go install github.com/gitleaks/gitleaks/v8@latest
# Docker, no local install needed
docker pull zricethezav/gitleaks:latest
# Verify the install
gitleaks version
# gitleaks version 8.30.1
If none of those fit, download the prebuilt binary for your platform directly from the Gitleaks GitHub releases page and place it somewhere on your PATH. On Linux CI runners without Homebrew or Go pre-installed, pulling the binary directly is usually faster than compiling from source.
Step 2: Run Your First Repository Scan
Navigate into any git repository and run a detection pass. Gitleaks defaults to scanning the entire commit history, not just the current working tree, which is the behavior you want for a first audit.
cd your-project
gitleaks detect --source . --verbose
# writing a machine-readable report alongside the console output
gitleaks detect --source . --report-format json --report-path gitleaks-report.json -v
A clean repository exits with status 0 and prints a short summary. A repository with findings exits with status 1, which is exactly the behavior you want a CI pipeline to react to later. Here is what a real finding looks like in the console:
Finding: AKIAIOSFODNN7EXAMPLE
Secret: AKIAIOSFODNN7EXAMPLE
RuleID: aws-access-token
Entropy: 3.643856
File: config/deploy.sh
Line: 14
Commit: a1b2c3d4e5f67890abcdef1234567890abcdef12
Author: jdoe@example.com
Date: 2026-02-11T09:14:22Z
10:31PM INF 1 commits scanned.
10:31PM WRN leaks found: 1
Note the commit hash and author in the output. That is what makes a full-history scan valuable over scanning only the latest commit: it tells you exactly when the exposure happened and who to loop in for the rotation, even if the line was deleted three commits later.
Step 3: Read a Gitleaks Report Correctly
Each finding in the JSON report carries a RuleID (which pattern matched), an Entropy score (how random the string looks, used to catch generic secrets that don’t match a known vendor format), and a Fingerprint field. The fingerprint is the important one for triage: it is a stable hash of the commit, file, and rule that identifies this exact finding uniquely. You will use fingerprints later to allowlist confirmed false positives without silencing the rule for the whole repository.
Treat every RuleID differently depending on category. A match on aws-access-token or stripe-access-token is a named vendor format and almost always real. A match on generic-api-key is entropy-based and carries a much higher false-positive rate, since it will also flag long randomized test fixtures, hashed passwords in test databases, and UUIDs. Start remediation with the named vendor findings first, since they are the ones with actual blast radius.
Step 4: Write a Custom .gitleaks.toml Ruleset
The default ruleset covers common vendors well, but every codebase has its own noise: internal service names that look like tokens, seeded test data, or a documented example key repeated across README files. A local config file extends the defaults instead of replacing them, which keeps you covered as the Gitleaks maintainers add new vendor patterns.
title = "custom gitleaks config"
[extend]
useDefault = true
[[rules]]
id = "internal-service-token"
description = "Internal service-to-service auth token"
regex = '''(?i)svc_token_[a-z0-9]{32}'''
tags = ["internal", "high-severity"]
[allowlist]
description = "Known-safe paths and documented example values"
paths = [
'''(?i)fixtures/''',
'''(?i)test/data/'''
]
regexes = [
'''AKIAIOSFODNN7EXAMPLE''',
'''sk_test_[0]{24}'''
]
Save this as .gitleaks.toml in the repository root and point every scan at it with --config .gitleaks.toml. Resist the urge to allowlist entire directories like vendor/ or node_modules/ broadly unless you have verified nothing sensitive lives there. A path-based allowlist that is too wide is one of the most common ways teams accidentally blind their own scanner.
Step 5: Scan Your Entire Git History
If this is the first time Gitleaks has touched this repository, run one deliberate full-history pass before wiring anything into CI. CI scans going forward will mostly check new commits. This one-time pass is what surfaces the years-old leak that everyone forgot about.
# Full clone required, shallow clones hide history
git clone --no-single-branch https://github.com/your-org/your-repo.git
cd your-repo
gitleaks detect --source . --config .gitleaks.toml --report-format json --report-path full-history-audit.json -v
# Count findings by rule to prioritize triage
python3 -c "
import json
data = json.load(open('full-history-audit.json'))
from collections import Counter
print(Counter(f['RuleID'] for f in data))
"
This is a slow scan on large, old repositories, sometimes taking several minutes on a repo with tens of thousands of commits. Run it once, save the report, and use it as your remediation backlog rather than re-running it on every push.
Step 6: Block Commits Locally With a Pre-Commit Hook
Catching a secret before it ever leaves a developer’s machine is cheaper than catching it in CI, because nothing has to be rotated, just deleted. The pre-commit framework is the standard way to wire this up across a team without asking every developer to configure git hooks manually.
# install the framework once per machine
pip install pre-commit
# .pre-commit-config.yaml, committed to the repo root
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
# each developer runs this once after cloning
pre-commit install
From this point, every git commit runs Gitleaks against the staged diff first. A match blocks the commit outright and prints the offending file and line. Keep in mind this only protects developers who ran pre-commit install locally, and it can be bypassed with git commit --no-verify, which is exactly why the next two steps enforce the same check in CI where it cannot be skipped.
Step 7: Enforce Scanning in CI/CD
Local hooks catch honest mistakes. CI enforcement catches everything else, including contributors who never installed the hook, forks, and direct pushes. Set this up on both the platform you use for pull requests and any secondary CI runner in your stack.
GitHub Actions
# .github/workflows/gitleaks.yml
name: gitleaks
on: [pull_request, push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run gitleaks
run: |
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz | tar -xz gitleaks
./gitleaks detect --source . --config .gitleaks.toml --exit-code 1 -v
The fetch-depth: 0 line matters more than it looks. GitHub Actions checks out a shallow clone by default, which means Gitleaks only sees the single commit that triggered the run and misses anything an attacker could have force-pushed earlier in the same PR branch.
GitLab CI
# .gitlab-ci.yml
gitleaks:
stage: test
image:
name: zricethezav/gitleaks:latest
entrypoint: [""]
script:
- gitleaks detect --source . --config .gitleaks.toml --exit-code 1 --verbose
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Set GIT_DEPTH to 0 in the GitLab CI/CD variables for this job as well, for the same shallow-clone reason. Once both pipelines are green, go back and add a branch protection rule requiring the gitleaks check to pass before a merge is allowed, otherwise a failing scan is just a warning nobody reads.
Step 8: Add TruffleHog for Live Credential Verification
Once Gitleaks is catching everything structurally, add TruffleHog as a second-stage scan that answers the question Gitleaks can’t: is this credential still active right now.
# install
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
trufflehog --version
# trufflehog 3.97.1
# scan a local repo, only surface verified, currently-working secrets
trufflehog git file://. --only-verified
# scan a remote repo directly by URL
trufflehog github --repo=https://github.com/your-org/your-repo --only-verified
Run this on a schedule rather than on every commit, since the verification API calls add real latency compared to Gitleaks’ offline pattern matching. A nightly GitHub Actions cron job or a weekly job ahead of a release cut is the usual cadence. When TruffleHog reports a verified hit, treat it as a live incident, not a backlog item, because it means the credential works right now.
Step 9: Triage a Confirmed Secret Leak
When a scan turns up a real finding, work the list in this order rather than alphabetically or by file name.
- Confirm the credential is still valid with TruffleHog or by testing it directly against the issuing service’s API.
- Revoke or rotate it at the source immediately, this step matters more than anything you do in git.
- Check the provider’s access logs for the exposure window to see if it was actually used by anyone besides you.
- Update the fingerprint into your allowlist only after rotation is confirmed complete, never before.
- Only then decide whether to rewrite git history to remove the string, since rotation alone already neutralizes the risk.
That ordering surprises teams new to this. Removing the string from git history feels like the obvious first move, but a secret that’s already been scraped by a bot is compromised the moment it’s exposed, not the moment someone finds it in your repo. Rotation is the fix. History rewriting is cleanup. The OWASP Secrets Management Cheat Sheet lays out the same rotation-first sequence for a reason: it is the order that actually limits damage, not the order that feels most satisfying to follow.
Step 10: Rotate the Secret and Rewrite Git History
After rotation is confirmed, remove the old value from history so it stops showing up in every future scan and every fork of the repository. git filter-repo is the maintained, recommended tool for this. The older git filter-branch is officially discouraged by the Git project for being slow and easy to misuse.
pip install git-filter-repo
# remove a specific file that contained the secret from all history
git filter-repo --path config/deploy.sh --invert-paths
# or replace a specific string across all history without dropping the file
echo 'AKIAIOSFODNN7EXAMPLE==>REDACTED' > replacements.txt
git filter-repo --replace-text replacements.txt
# force-push the rewritten history
git push origin --force --all
git push origin --force --tags
Every collaborator with an existing clone needs to know before you do this. A force-push after a history rewrite invalidates every local clone’s history. The standard message to send is “re-clone the repository, don’t try to pull or rebase.” Skipping that communication step is the single most common way this goes wrong in practice.
Step 11: Schedule Continuous Scanning and Alerts
CI hooks catch new commits. A scheduled scan catches everything that slipped through before the hooks existed, plus anything that reaches the repo through a path CI doesn’t cover, like a direct push from an admin account with bypass permissions.
# .github/workflows/gitleaks-nightly.yml
name: gitleaks-nightly-audit
on:
schedule:
- cron: '0 3 * * *'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Full history audit
run: |
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz | tar -xz gitleaks
./gitleaks detect --source . --report-format json --report-path nightly-audit.json -v || true
- name: Notify on findings
if: failure()
run: echo "Post nightly-audit.json to your team's incident channel here"
Wire the notification step to whatever your team already uses for alerts, Slack, PagerDuty, or an internal ticketing webhook. The goal is that a finding reaches a human within hours, not that it sits in a report file nobody opens.
Most of the damage from a secret-scanning program is not the tool failing to detect something. It is the surrounding process failing: an allowlist entry nobody reviews, a hook nobody installed, a report nobody reads. Every pitfall below has shown up repeatedly across teams that ran a working scanner and still leaked a credential anyway.
Common Pitfalls That Undermine Secret Scanning
- Scanning only the latest commit. A secret deleted in the next commit is still sitting in history, and without a full-history scan you never see it.
- Treating detection as the fix. Deleting the line from the codebase does nothing to the credential itself. Rotation is the only step that actually neutralizes risk.
- Allowlisting entire directories. Excluding
vendor/ortest/wholesale to cut noise also hides the one time a real secret gets copy-pasted into a fixture file. - Relying on local hooks alone. Pre-commit hooks are opt-in and bypassable with
--no-verify. Without a CI gate, they only stop honest mistakes from careful developers. - Ignoring shallow clones in CI. Default checkout depth on both GitHub Actions and GitLab CI hides history a full scan needs to see.
- Running TruffleHog without
--only-verified. Unfiltered output buries the handful of live secrets in hundreds of unranked possible matches, and teams stop reading the report within a week.
Troubleshooting Gitleaks and TruffleHog
The table below covers the issues that come up most often once a team moves from a first test scan to a pipeline running in production. Most of them trace back to two root causes: a shallow git checkout that hides the history a scan needs, or a configuration file that suppresses more than it was meant to.
| Symptom | Likely cause | Fix |
|---|---|---|
| “gitleaks: command not found” | Binary not on PATH after manual download | Move the binary to /usr/local/bin or add its directory to PATH |
| Scan reports zero findings on a known-leaked repo | Shallow clone hides history, or an allowlist entry matches too broadly | Re-clone with full history or run with --log-opts="--all"; review .gitleaks.toml allowlist regexes |
| CI passes locally but fails in GitHub Actions | Actions defaults to a shallow checkout | Add fetch-depth: 0 to the actions/checkout step |
| Hundreds of false positives from test fixtures | Generic entropy-based rules flagging random test data | Add path or fingerprint-based allowlist entries in .gitleaks.toml, or inline # gitleaks:allow comments |
| Pre-commit hook never runs | Developer cloned the repo but never ran pre-commit install |
Add a setup script or onboarding doc step that runs it automatically |
| gitleaks protect finds nothing in CI | protect mode scans staged/uncommitted changes only, not full history |
Use gitleaks detect in CI, reserve protect --staged for local pre-commit hooks |
| TruffleHog exits 0 despite verified findings | Default exit behavior doesn’t fail the build on verified secrets | Add the --fail flag so CI treats verified findings as a pipeline failure |
| Report shows the finding but redacts the secret value | Default redaction for safety when sharing report output | Only disable redaction in a locked-down, access-controlled environment, never in shared CI logs |
| History rewrite breaks every collaborator’s local branch | Force-push after filter-repo invalidates existing clones | Notify the team before force-pushing and have everyone re-clone rather than pull |
| Docker scan can’t find the repo path | Volume mount path doesn’t match the –source flag inside the container | Match the mount and source paths exactly, e.g. -v $(pwd):/repo with --source=/repo |
Advanced Tips for Mature Secret-Scanning Programs
Once the basic pipeline is stable, a baseline file keeps the scanner useful instead of noisy. Running gitleaks detect --baseline-path previous-report.json tells Gitleaks to only report new findings since the last audit, which matters once you have a backlog of already-triaged historical leaks that would otherwise reappear on every scan and drown out genuinely new commits.
For teams running dozens of repositories, centralize the .gitleaks.toml ruleset in one internal repo and reference it from every project’s CI job instead of copy-pasting the file everywhere. That way a new internal token pattern or an updated allowlist entry rolls out once instead of requiring a pull request against every service. Ordering also matters inside a broader AppSec pipeline: run secret scanning ahead of SAST and SCA checks, since a live cloud credential is a more urgent finding than a vulnerable dependency and should surface first in a build log a developer might not read past the first failure.
Finally, pair scanning with a secrets manager so there’s a destination once a rotated credential needs a new home. Storing rotated keys in HashiCorp Vault rather than a fresh environment variable or config file closes the loop, since the same mistake that caused the original leak (a plaintext credential in a file that gets committed) is otherwise free to happen again with the new key.
Where Secret Scanning Fits Into a Broader AppSec Program
Secret scanning is one layer in a stack, not a replacement for the rest of it. If you have not already covered the adjacent basics in your cybersecurity pipeline, treat this tutorial as the starting point rather than the finish line. A container built from a compromised base image can leak the exact same class of credential through a baked-in environment file, which is why teams that run Gitleaks against source code also tend to scan container images with Trivy before anything ships to a registry. Trivy catches secrets and known vulnerabilities baked into image layers that never touch a git commit at all.
The two tools cover genuinely different surfaces, so running one is not a substitute for the other. Gitleaks and TruffleHog watch the git history and every incoming diff. Trivy watches the built artifact. A CI pipeline that only checks one of the two will still ship a compromised container built from a clean commit history, or a clean container built from a leaky one. Most teams that get this right run both as separate, parallel jobs rather than trying to force one tool to do the other’s job.
Complete Working Project: A Secrets-Safe CI Pipeline
Putting every step together, a repository with secret scanning fully wired in looks like this:
your-project/
├── .gitleaks.toml # custom rules + allowlist, extends defaults
├── .pre-commit-config.yaml # local hook, blocks commits before they happen
├── .github/
│ └── workflows/
│ ├── gitleaks.yml # runs on every push and pull_request
│ └── gitleaks-nightly.yml # full-history audit + TruffleHog verification, 3am daily
├── .gitleaksignore # fingerprints of confirmed false positives only
└── src/
The nightly job is the piece most tutorials skip, so here is the full version combining both tools in one workflow:
name: gitleaks-nightly-audit
on:
schedule:
- cron: '0 3 * * *'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Gitleaks full history scan
run: |
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz | tar -xz gitleaks
./gitleaks detect --source . --config .gitleaks.toml --report-format json --report-path gitleaks-audit.json -v || true
- name: TruffleHog verified-only scan
run: |
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
trufflehog git file://. --only-verified --fail --json > trufflehog-audit.json || echo "verified secrets found"
- name: Upload reports
uses: actions/upload-artifact@v4
with:
name: secret-scan-reports
path: |
gitleaks-audit.json
trufflehog-audit.json
With this in place, a fast Gitleaks pass runs on every single push, catching the vast majority of mistakes within seconds. A slower TruffleHog pass runs nightly, confirming which of any accumulated findings are actually live and worth an urgent rotation call. Between the two, a leaked credential has a very short window to sit unnoticed, which is the entire point of the exercise. None of the individual pieces here are complicated on their own. What makes the setup effective is that every layer runs automatically, without depending on a developer remembering to do anything by hand.
Frequently Asked Questions
What’s the actual difference between Gitleaks and TruffleHog?
Gitleaks is a fast, offline pattern matcher that flags anything shaped like a secret. TruffleHog does the same pattern matching but adds live API verification, actually checking whether a flagged credential still works against the service it belongs to. Gitleaks tells you what looks like a secret, while TruffleHog tells you which ones are real problems right now.
Does Gitleaks scan private repositories, or only public ones?
Gitleaks runs entirely locally or inside your own CI runner against whatever repository you point it at, public or private. Nothing is sent to a third-party service unless you’re using a hosted platform’s built-in scanning feature instead of the open-source CLI.
Can Gitleaks find a secret that was committed and then deleted in a later commit?
Yes, and this is the main reason to run a full-history scan rather than checking only the current working tree. Git retains every prior version of every file in its object database, so a secret removed from the latest commit is still fully retrievable from an earlier one unless the history itself is rewritten.
Is Gitleaks really free for commercial, for-profit use?
The Gitleaks CLI itself is MIT-licensed and free for any use, commercial included. The one paid component is the official gitleaks-action for GitHub Actions, which requires a license key specifically for organization-owned repositories. Running the open-source binary directly in a CI script, as shown in this tutorial, avoids that requirement entirely.
How do I stop Gitleaks from constantly flagging test fixtures as false positives?
Add specific fingerprint or path entries to the allowlist section of your .gitleaks.toml, or drop an inline # gitleaks:allow comment on the exact line. Avoid allowlisting an entire directory unless you’ve confirmed nothing sensitive will ever land there, since that same exclusion will hide a real leak just as effectively as a false one.
What’s the very first thing I should do if a scan finds a real, live secret?
Revoke or rotate the credential at its source immediately, before touching git history at all. A key that has already been pushed to a remote repository should be treated as compromised the moment it was exposed, regardless of whether anyone has visibly used it yet. History cleanup is a follow-up step, not the fix.
Does GitHub’s built-in secret scanning make a tool like Gitleaks unnecessary?
GitHub’s native secret scanning is a useful safety net, particularly for partner-verified patterns that trigger automatic revocation with some providers, but it runs on GitHub’s schedule and rule set, not yours. Gitleaks gives you a custom, extensible ruleset you control directly in your own pipeline, works identically on GitLab or any self-hosted git server, and can enforce a hard block before a merge rather than an after-the-fact alert.
How often should I run a full-history audit versus a normal diff scan?
Run the diff-style scan (Gitleaks against just the new commits) on every push and pull request, since it’s fast and catches new mistakes immediately. Reserve the full-history audit combined with TruffleHog’s verification pass for a nightly or weekly schedule. It’s slower, and its real job is catching anything that slipped through before the automated checks existed.