For: anyone using Git in solo or small-team development, worried about "what if I commit an API key by accident?" No attacker mechanics here — just the machinery for stopping secrets from leaking out of your own repository.
This site's view: .gitignore alone can't protect secrets
"My .env is in .gitignore, so I'm fine" is a common line, and it misses cases. .gitignore only keeps new files from being tracked; it can't catch secrets already committed, ones force-added in a hurry with git add -f, or unexpected names like .env.local.bak. It prevents only some of the ways a secret gets in. You need something that actually looks at commit contents and flags "secret-looking strings" — a detector. That's what gitleaks is for.
Why stop it "before the commit"
Secrets are dangerous because Git keeps history forever. Delete it from the latest file and it remains in past commits. And the moment it's pushed, that history is copied to other machines, servers, and CI logs.
So secret defense is really about prevention before the leak, not recovery after it. It's the same idea as the "don't leave secrets in a public directory" inventory (→ the webroot inventory), applied to code.
At which stages to detect secrets
On the path a secret takes from code to the world, you can place two checks: at the moment of the local commit, and just before it's shared (CI).
secret in code
API key, key, token
check 1: pre-commit
detect & abort locally at commit
check 2: CI / cron
detect after push, before merge/deploy
public / shared
past here, treat as leaked
How to wire gitleaks in
Scan existing history first
gitleaks git (gitleaks detect in versions before v8.19.0). Finding secrets left in past commits is the first job. Anything found here is, as below, assumed to need revocation.Add check 1 with a pre-commit hook
gitleaks git --pre-commit --staged (formerly gitleaks protect --staged) on every commit to abort on the spot any commit containing a secret. Add it to a pre-commit framework (.pre-commit-config.yaml) so the same check applies for everyone, locally.Add check 2 in CI / cron
gitleaks git as a CI job — or, for self-hosted setups, as a periodic scan — to catch what slips through (the same machine-monitoring mindset as dependency audits).Tune false positives in .gitleaks.toml
.gitleaks.toml. The right practice is not "suppress the warning" but "record why it's safe" — an exclusion with no rationale can cause the next incident.On detection, 'revoke' comes first; rewriting history comes after
When you find a secret that was committed and pushed, the top priority is revoking and reissuing (rotating) that key or token. Erasing it from history with git filter-repo matters, but it's only cleanup to "stop it spreading further." Assume a secret that reached a public repo, a fork, a CI log, or someone else's clone is unrecoverable — invalidate it first, then clean history. Reverse the order and it gets used while you're still deleting.
Relying on .gitignore
- only prevents new tracking
- secrets already committed go undetected
- powerless against
git add -for odd file names - can't verify "I didn't mean to add that"
gitleaks (detection + revocation practice)
- actually inspects the working tree and history contents
- stops it with two checks: pre-commit and CI
- on detection, a procedure that runs through to revocation
- false positives excluded with a recorded rationale
Which command to use (v8.19.0 and later)
gitleaks reorganized its commands in v8.19.0. The old detect and protect still run but are deprecated and no longer appear in the help. For a new setup, use these:
| Command | What it scans | Typical use |
|---|---|---|
gitleaks git | Git commit history | First full-history scan, CI, scheduled scans |
gitleaks git --pre-commit --staged | The changes you are about to commit (staged) | pre-commit hook (the official hook runs exactly this) |
gitleaks dir | Folders and files, including ones not in Git | Public web folders, backups, unpacked release zips |
gitleaks stdin | Text piped in on standard input | Output or logs from other commands |
gitleaks exits with code 1 when it finds at least one leak and 0 when it finds none, so CI and cron jobs can fail on that exit code. Add --redact so the secret itself never lands in the output or the logs.
Example setup: pre-commit hook and GitHub Actions
With the pre-commit framework, put this file at the root of the repository. Set rev to the latest release when you add it (v8.30.1 as of October 2026).
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaksBesides gitleaks (built with Go), the id can be gitleaks-docker (runs in Docker) or gitleaks-system (uses a gitleaks already installed on the machine). Then everyone runs this on their own machine:
pip install pre-commit # install pre-commit itself
pre-commit install # register the hook in this repo (once per clone)
pre-commit run --all-files # check every file once, right after registering
pre-commit autoupdate # run now and then to move rev to a newer releaseFor CI on GitHub, there is an official GitHub Action. It needs the full history, so check out with fetch-depth: 0.
# .github/workflows/gitleaks.yml
name: gitleaks
on:
pull_request:
push:
schedule:
- cron: "0 4 * * *"
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}For repositories owned by an organization account, the Action needs a license key (GITLEAKS_LICENSE). The official README says the key is free from gitleaks.io and is not needed for personal accounts. On a self-hosted Git server or a CI other than GitHub's, running gitleaks git --redact inside the job does the same thing.
Where to check: four places compared
Any single check leaves a gap. Laying out what each one stops, and where it leaks, shows why you combine them.
| Where | Stops it at | What it covers | Gaps and limits |
|---|---|---|---|
| pre-commit hook (local) | Before the commit | Staged changes | Skipped with git commit --no-verify or SKIP=gitleaks. Cloning a repo does not install the hook |
| CI (GitHub Actions etc.) | Before merge or deploy | Pushed commits and history | The push has already happened. On a public repo, treat it as leaked |
| Scheduled scan (cron) | Finds it later | Full repository history | Delay until discovery. Catches what new rules find in old commits |
| GitHub push protection | At push to GitHub | Secret types GitHub supports | Can be bypassed by giving a reason. Does nothing on other Git servers |
GitHub push protection for users is on by default for pushes to public repositories. It is a free safety net, but it does not stop local commits, and keys in unsupported formats or hand-built connection strings can get through (more in why credentials exposed on GitHub are still valid). gitleaks covers the step before that, and everything outside GitHub.
How to check your own setup
Setting it up once doesn't mean it is running. In your own repository, check these in order.
Check the gitleaks version
Run gitleaks version. Anything older than v8.19.0 lacks gitleaks git and the other new commands, so update it.
Check the hook is registered
Look for .git/hooks/pre-commit. If git config --get core.hooksPath points to another folder, look there. Git does not copy hooks when you clone, so run pre-commit install again on a new machine or a fresh clone.
Scan the whole history once
From the repository root, run gitleaks git --redact -v. With no findings the exit code is 0. If something is found, use the file name and commit in the output to check, in the issuer's console, whether that key is still valid.
Scan folders outside Git too
The public folder on your server, backups and the contents of zips you ship are invisible to a Git history scan. Check them with gitleaks dir (path to folder) --redact (→ the public-directory inventory).
Check whether .env was ever committed
Run git log --all --oneline -- .env to see whether any past commit touched .env. If even one line appears, the contents from that point are in the history.
Handling false positives
Dummy test keys and examples in documentation often get flagged. There are three ways to exclude them; use the narrowest one that works.
- Per line: add a
gitleaks:allowcomment on that line - Per finding: add the fingerprint shown in the output to
.gitleaksignoreat the repository root (the README marks this as experimental) - Per file or pattern: add it to the allowlist in
.gitleaks.toml
If you write your own .gitleaks.toml, put useDefault = true under [extend] to keep the default rules. A config with only your own rules silently drops the keys the defaults would have caught.
If an old repository has many existing findings, pass a past report to --baseline-path so only new findings are reported. Revoke the keys in that report first, though. Putting a key in the baseline does not make it safe.
Common mistakes and the fix
- Assuming the config file is enough: the hook does nothing until someone runs
pre-commit install. Have everyone run it, and run the same check in CI as a second layer. - Getting used to
--no-verify: make the CI check a required condition for merging, so a skipped hook is still caught. - Printing secrets into CI logs: add
--redact. CI logs are readable by many people too. - Scanning a shallow clone in CI: only the latest commit is checked and secrets in history are missed. Set
fetch-depth: 0. - Stopping after deleting it from history: GitHub's own guide makes revoking or rotating the secret the first step. Copies in forks, clones and caches cannot be removed.
- Allowlisting a real key as a "false positive": write the reason in a comment and check it in review.
A real case: an access key left public
On October 7, 2022, Toyota Motor Corporation announced that part of the source code for the user website of its T-Connect connected-car service had been public on GitHub. According to the company's announcement:
- In December 2017, the contractor developing the site mistakenly uploaded part of the source code to a GitHub account set to public
- The code contained an access key to the data server that stored customer information
- The code was made private on September 15, 2022, and the access key was changed on September 17
- 296,019 email addresses and customer management numbers were affected. The access logs showed no third-party use, but the company said it could not rule it out completely
That is about five years between upload and removal. It also happened outside the company's own repositories, in a contractor's account. A scanner on your own repos cannot reach every place your code ends up, so limit what each key can do and rotate keys on a schedule as well. And even here, the key was changed two days after the code was taken down: find it, then replace the key.
A private repository is not a safe place for secrets either. In CAMPFIRE's 2026 case, according to the company, a misused GitHub credential was the starting point, and information viewable on GitHub was used to obtain cloud credentials (→ the CAMPFIRE breach). Keep secrets out of private repos as well, and scan them with gitleaks on a schedule.
What this site does itself
This site's basic rule is keeping secrets out of Git entirely — connection strings and API keys live only in protected config on the server (a permission-restricted env file), never in plaintext in the repo or in handoff notes (→ keeping secrets out of git / self-hosted Git vs GitHub). On top of that, we layer detectors on the assumption that humans will always slip. The design guarantees "don't put it in," and a scan like gitleaks catches "it got in anyway" — those two layers are this site's defense against secret leaks. The principle of handling secrets itself is the same as in .env files and secrets and storing passwords safely.
Read next
- GitHub settings: How to set up GitHub two-factor authentication
- Basics: .env files and API keys: what goes wrong and how to protect them
- From a GitHub credential to a breach: the CAMPFIRE breach (2026)
- After a leak: 543,699 credentials exposed on GitHub are still valid — why you must revoke, not just delete
- When the attack ran without a human: the Hugging Face intrusion and the assumption defenders must drop
- Inventory: are you leaving secrets in a public directory
- Self-hosting: keeping secrets out of git / self-hosted Git vs GitHub
- Glossary: what ".env" is — what happens when an env file leaks
- Tool: secret leak scanner (paste and check)
Sources
- gitleaks (official repository) README: commands (git, dir, stdin), deprecation of detect/protect in v8.19.0, exit codes,
--redact,gitleaks:allow,.gitleaksignore,--baseline-path, config extension — github.com/gitleaks/gitleaks / official hook definitions — .pre-commit-hooks.yaml / Releases - gitleaks-action (official GitHub Action) README: example workflow and license key — github.com/gitleaks/gitleaks-action
- pre-commit documentation (install, run --all-files, autoupdate, SKIP) — pre-commit.com
- Git documentation, "githooks" (the pre-commit hook and --no-verify) — git-scm.com
- GitHub Docs, "About push protection" — docs.github.com / "Removing sensitive data from a repository" — docs.github.com
- Toyota Motor Corporation, notice on the possible leak of customer email addresses (October 7, 2022, in Japanese) — global.toyota