Security Guides

Stop secrets before they commit with gitleaks: catch API-key leaks before the push

A committed API key stays in Git history for good. How gitleaks stops secrets with a pre-commit hook and CI, plus the key revocation detection doesn't do.

Published 2026-06-12 Updated 2026-10-06 15 min read
On this page

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.

stays in history
gone from latest, still in past commits
push = spread
copied to other machines, servers, CI logs
must revoke
a once-leaked key can only be reissued
two checks
stop it at pre-commit and CI

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

The leak path and two checks. Pre-commit stops it locally; CI stops it before merge/deploy.

How to wire gitleaks in

1

Scan existing history first

On adoption, scan the whole repo history once with 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.
2

Add check 1 with a pre-commit hook

Run 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.
3

Add check 2 in CI / cron

Hooks can be disabled by each person, so check again before anything is shared. Run 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).
4

Tune false positives in .gitleaks.toml

When dummy test keys or sample values trip it, adjust rules and the allowlist in .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 -f or 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:

CommandWhat it scansTypical use
gitleaks gitGit commit historyFirst full-history scan, CI, scheduled scans
gitleaks git --pre-commit --stagedThe changes you are about to commit (staged)pre-commit hook (the official hook runs exactly this)
gitleaks dirFolders and files, including ones not in GitPublic web folders, backups, unpacked release zips
gitleaks stdinText piped in on standard inputOutput 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: gitleaks

Besides 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 release

For 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.

WhereStops it atWhat it coversGaps and limits
pre-commit hook (local)Before the commitStaged changesSkipped with git commit --no-verify or SKIP=gitleaks. Cloning a repo does not install the hook
CI (GitHub Actions etc.)Before merge or deployPushed commits and historyThe push has already happened. On a public repo, treat it as leaked
Scheduled scan (cron)Finds it laterFull repository historyDelay until discovery. Catches what new rules find in old commits
GitHub push protectionAt push to GitHubSecret types GitHub supportsCan 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.

1

Check the gitleaks version

Run gitleaks version. Anything older than v8.19.0 lacks gitleaks git and the other new commands, so update it.

2

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.

3

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.

4

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).

5

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:allow comment on that line
  • Per finding: add the fingerprint shown in the output to .gitleaksignore at 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.

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

FAQ

QWhat is gitleaks?
A

A free tool that scans a Git repository's working tree and commit history to detect whether secrets — API keys, private keys, tokens — have been committed. It finds 'secret-looking strings' with regex rules and string entropy (randomness), and can run as a pre-commit hook or in CI for automatic checks.

QDoesn't putting it in .gitignore protect secrets?
A

Not enough. .gitignore only stops 'new tracking' — it can't detect secrets that are already committed, or ones force-added with git add -f. It prevents only some accidents, so pair it with a detector like gitleaks in both pre-commit and CI.

QWhat if I accidentally committed and pushed a secret?
A

Treat that secret as leaked. The top priority is not rewriting history but revoking and rotating (reissuing) the exposed key or token. Assume a secret that hit a public repo or a log can't be recalled — invalidate it first, then clean up history and add prevention (gitleaks).

QDo gitleaks detect and gitleaks protect still work?
A

They still run, but they were deprecated in v8.19.0 and are hidden from the help output. The current commands are gitleaks git (scans commit history), gitleaks dir (scans folders and files) and gitleaks stdin (scans piped input). The pre-commit check is now gitleaks git --pre-commit --staged, which is also what the official pre-commit hook runs. Use the new commands when you set things up.

QIf GitHub push protection is on, do I still need gitleaks?
A

They cover different things, so use both. GitHub push protection for users is on by default for pushes to public repositories, but it can be bypassed by choosing a reason, and it does nothing for local commits or for Git servers other than GitHub. gitleaks stops the secret on your machine before the commit, and runs the same rules on a self-hosted Git server or any CI.

QHow do I stop gitleaks flagging a dummy test key?
A

There are three ways: add a gitleaks:allow comment on that line, put the finding's fingerprint in .gitleaksignore, or exclude files or patterns with an allowlist in .gitleaks.toml. Whichever you use, leave a comment saying why it is safe. The failure to avoid most is allowlisting a real key as a false positive.