Detection guide
How to scan a git repository for secrets and API keys
Git history is a common hiding place for secrets that were committed months or years ago, removed from the working tree, and forgotten. This guide covers four layers of scanning — a quick browser check for a single file, a full history audit with open-source CLI tools, a pre-commit hook to stop new secrets at the source, and a GitHub Actions job to enforce it in CI.
- Why history scanning matters
- Quick scan: paste check
- Full history: gitleaks
- Full history: trufflehog
- Pre-commit hook
- GitHub Actions
- Found a secret — now what?
- FAQ
Why git history scanning matters
The standard fix for an accidentally committed secret is to delete the file, add it to .gitignore, and push a new commit. This removes the secret from the working tree but leaves it intact in every historical commit. Anyone with read access to the repository — including all past and current collaborators — can run git checkout <old-sha> -- .env and read the original value. The secret is still live in the repo; it is just one git log away.
There are two distinct problems worth scanning for:
- Secrets already in history. Old commits, branches, or tags that still contain credentials that were rotated (or were supposed to have been rotated) at some point in the past.
- Secrets about to enter history. Staged changes — the next commit — that contain credentials the developer forgot to move to an environment variable.
Different tools are best suited to each. The table below shows how they map:
| Use case | Best tool | What it examines |
|---|---|---|
| Quick check of a single file or diff | LeakCheck (browser) | Pasted text, no install needed |
| Full audit of git history | gitleaks detect | All commits, all branches, all tags |
| Full audit with entropy + verified matches | trufflehog | All commits + live credential verification |
| Block new secrets before committing | gitleaks protect (pre-commit hook) | Staged diff only |
| Enforce in CI on every push | gitleaks-action or trufflehog GitHub Action | Commits in the pushed branch |
Quick scan: paste a file or diff into LeakCheck
If you want to check a specific file, config snippet, or staged diff without installing anything, LeakCheck runs 40+ credential patterns in the browser. Your code is never sent to a server — all matching happens client-side.
Common things to paste in:
- A
.envfile you are about to copy to a new project - The output of
git diff --cachedbefore committing - A CI configuration file that might have hardcoded credentials
- A config file returned by a vendor that might include an embedded key
Paste and scan with LeakCheck →
For scanning the full commit history of a repository — potentially hundreds or thousands of commits — you need a CLI tool that can walk the git object store. That is what gitleaks and trufflehog do.
Full history audit with gitleaks
gitleaks is the most widely used open-source secret scanner for git repositories. It is written in Go, ships as a single binary, and scans your local .git directory with no network requests required.
Install
# macOS (Homebrew)
brew install gitleaks
# Linux (download binary from GitHub releases)
curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64.tar.gz \
| tar -xz -C /usr/local/bin gitleaks
# Windows (Scoop)
scoop install gitleaks
# Verify
gitleaks version
Scan the full history
Run the following from the root of the repository you want to audit. The detect subcommand walks all commits across all branches and tags.
# Scan all commits in the current repo
gitleaks detect --source . --log-opts="--all"
# Write results to a JSON report file
gitleaks detect --source . --log-opts="--all" --report-path gitleaks-report.json
# Include specific branches only
gitleaks detect --source . --log-opts="main..feature/my-branch"
Each finding includes the file path, line number, the matched rule (e.g. aws-access-token), the commit SHA, the author, and a redacted excerpt. Exit code 1 means at least one secret was found; exit code 0 means clean.
.gitleaks.toml at the root of the repository. Use [[allowlist.commits]] to exempt specific commit SHAs, or [[allowlist.paths]] to exclude paths like tests/fixtures/ or docs/examples/. The gitleaks configuration docs cover the full schema.
Full history audit with trufflehog
trufflehog (by Truffle Security) takes a different approach: it uses entropy analysis to catch secrets that do not match a known pattern, and — uniquely — it can verify discovered credentials against the live API to confirm they are still valid before reporting them.
Install and run
# macOS (Homebrew)
brew install trufflehog
# Linux / macOS (install script)
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh \
| sh -s -- -b /usr/local/bin
# Scan local repo (all commits), only report verified live secrets
trufflehog git file://. --only-verified
# Scan all commits including unverified findings
trufflehog git file://. --no-verification
# Scan from a specific commit forward
trufflehog git file://. --since-commit <sha>
The --only-verified flag is the key differentiator: trufflehog contacts each provider's API with the discovered credential to confirm it is still active. A verified finding is a live, working secret. An unverified finding is a pattern match that may or may not still be valid — still worth rotating, but with lower urgency than a confirmed active credential.
--no-verification.
Pre-commit hook: stop secrets before they enter history
A pre-commit hook runs before git records a commit. Using gitleaks protect in a hook means that if you try to stage a file containing a secret pattern, the commit is rejected before the secret ever reaches your local history — let alone the remote.
Option A: install with the pre-commit framework
The pre-commit framework manages hooks across your team so everyone gets the same configuration from the repo.
# Install the framework
pip install pre-commit # or: brew install pre-commit
# Add to .pre-commit-config.yaml in your repo root
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4 # use latest tag
hooks:
- id: gitleaks
# Install hooks into the repo
pre-commit install
Option B: raw git hook (no framework)
# Write the hook directly
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
gitleaks protect --staged -v
EOF
chmod +x .git/hooks/pre-commit
With either option, the next time you run git commit, gitleaks scans the staged diff (git diff --cached). If it finds a match, the commit is aborted and the offending finding is printed with the file and line number.
GitHub Actions: enforce on every push
A pre-commit hook protects your local machine; a CI check catches anything that slips through (for instance, if a team member has not installed the hook). Add one of these jobs to your workflow:
Using gitleaks-action
# .github/workflows/secret-scan.yml
name: Secret scan
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history — required for gitleaks to scan all commits
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # only required for private repos on the free plan
Using trufflehog
# .github/workflows/secret-scan.yml
name: Secret scan
on: [push, pull_request]
jobs:
trufflehog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
extra_args: --only-verified
fetch-depth: 0 is required. By default, actions/checkout does a shallow clone (last commit only). Both gitleaks and trufflehog need the full history to audit previous commits. Always set fetch-depth: 0 in the checkout step, otherwise the scan only covers the single commit that triggered the run.
Found a secret — what now?
If any of the above scans returns a finding, the response is the same regardless of how the secret got there: rotate it at the provider before anything else. Git history rewriting is the second step — not the first. A revoked key is useless to an attacker even if it remains in history; an unrevoked key in history is still live even after you rewrite the commits.
The full remediation sequence — provider-by-provider rotation, history cleanup with git-filter-repo and BFG, and prevention steps — is covered in the companion guide:
Full remediation guide: accidentally committed API key →
Frequently asked questions
Can gitleaks scan a private GitHub repository?
Yes. gitleaks runs entirely locally on your cloned repository — it reads the .git directory and makes no network calls during a scan. It scans whatever you have cloned, regardless of the repository's visibility on GitHub. gitleaks also supports scanning all repos in a GitHub org or user account via gitleaks detect --github-org <org>, which uses your personal access token to enumerate and clone them.
What is the difference between gitleaks detect and gitleaks protect?
gitleaks detect audits the full commit history — every commit on every branch and tag. gitleaks protect scans only the currently staged changes (the diff that would be recorded by the next git commit). Use detect for a repository audit; use protect in a pre-commit hook to stop new secrets from being committed in the first place.
Does scanning git history delete the secrets it finds?
No. Scanners only read and report; they never modify your repository. To remove a secret from git history, you need to rewrite the commit graph using git filter-repo or BFG Repo-Cleaner and force-push the result. The scanner gives you the commit SHAs and file paths; the remediation guide covers the actual removal steps.
Will a pre-commit hook catch secrets in files that are not staged?
No. gitleaks protect --staged only scans the output of git diff --cached — the exact diff that will be recorded in the commit. Unstaged changes, untracked files, and anything already in history are not examined by the hook. For comprehensive coverage, combine the hook (prevents new additions) with a periodic gitleaks detect --log-opts="--all" run against the full history.
How do I suppress false positives from test files or example configs?
Create a .gitleaks.toml file at the root of the repository. You can allow specific paths (e.g. tests/, docs/examples/), specific commit SHAs (for known historical false positives), or regex patterns that match placeholder strings like EXAMPLE_KEY or sk_test_. The full gitleaks configuration reference documents all allowlist options.
Does GitHub Secret Scanning replace running my own scanner?
Not entirely. GitHub scans new commits pushed to public repositories (and private repositories with GitHub Advanced Security). It does not run before you commit, does not scan your full history before you push, and its pattern set covers a fixed list of providers. A pre-commit hook stops secrets before they reach any remote; a CLI history scan covers what predates the hook. GitHub's scanning is a useful additional layer, not a replacement for either.
Also in the Copper Bay Labs ship-safety suite
Secret scanning is one layer of a broader pre-deploy security posture. These free tools cover the rest:
Scan your live URL for exposed .env files, .git directories, source maps, and bundled secrets. Free, no signup.
Check your security headers — CSP, HSTS, X-Frame-Options, and more — so your site does not become an XSS vector.
Dependencies DepCheckScan your package.json for vulnerable, outdated, and abandoned npm packages before they reach production.