How to Catch Leaked Secrets Before They Hit GitHub With Gitleaks
A beginner's guide to installing Gitleaks, scanning a repository for leaked secrets, and blocking future leaks automatically with a pre-commit hook and GitHub Actions.
On July 10, 2026, TechCrunch reported that the U.S. Cybersecurity and Infrastructure Security Agency (CISA) admitted it did not have a prepared incident response plan ready when it needed one. The admission traced back to an incident from May 2026: a security researcher at GitGuardian found reams of exposed passwords and credentials for U.S. government systems sitting in a publicly accessible GitHub repository, uploaded there by an employee of a CISA contractor, and tried to alert the contractor directly, but got no response. Independent journalist Brian Krebs reported on the researcher’s findings that same month, and only after Krebs contacted CISA itself did the agency take the repository offline and revoke and reissue every exposed credential. In its own postmortem, CISA admitted that its staff “had to spend time building [a playbook] during the early stages of the incident,” and said organizations should prepare playbooks “for all anticipated needs” instead of improvising one under pressure.
Table Of Content
- What counts as a secret, and why git makes leaks permanent
- What you will need
- Step 1: Install Gitleaks
- Download and verify the binary
- Install it and confirm the version
- Step 2: Run your first scan
- Step 3: Decide what a finding means
- Step 4: Silence false positives without disabling the rule
- Option A: an inline allow comment
- Option B: a .gitleaksignore file
- Step 5: Install the pre-commit framework
- Step 6: Block leaks automatically with a pre-commit hook
- Write the configuration file
- Activate the hook
- Confirm it actually blocks a leak
- Confirm a clean commit still works
- Step 7 (optional): add a second layer in GitHub Actions
- Common mistakes and gotchas
- How to verify everything works end to end
- Next steps
That kind of leak is not rare, and it is not a CISA problem specifically. It happens because most teams have no automatic, low-effort way to catch a secret before it is committed to git, let alone before it is pushed to a public repository. This tutorial builds exactly that safety net. You will install Gitleaks, a free and open source secret scanner (unrelated to GitGuardian, the company in the CISA story above; they are two separate tools in the same space), run it against a real repository, teach it to ignore your false positives, and then wire it into a pre-commit hook so a leak is blocked on your own machine before it ever reaches GitHub. As a bonus step, you will also add a second layer in GitHub Actions, because local hooks alone can be skipped. Every command below was run and verified against a real test repository while writing this tutorial, and every tool version was checked against its official release before publishing.
What counts as a secret, and why git makes leaks permanent
A “secret” in this context is any credential that grants access to something: an API key, a database password, a private key file, a cloud provider’s access token, a webhook signing secret. Gitleaks finds these by combining two techniques. First, it matches text against known patterns for specific services (an AWS access key always starts with AKIA, a GitHub personal access token always starts with ghp_, and so on). Second, for anything that does not match a known service, it calculates the entropy of a string, a measure of how random it looks, since a real password or key is far more random-looking than an ordinary word or sentence. A string assigned to a suspicious variable name (like api_key or secret) that also scores high on entropy gets flagged even if it does not belong to a service Gitleaks specifically recognizes.
The reason this matters more in git than in a normal filesystem is that git is designed to never truly forget. If you commit a secret and then delete it in a later commit, the secret is still sitting in your repository’s history, in the earlier commit, forever, unless you deliberately rewrite history and force-push. Worse, anyone who already cloned or forked the repository before you noticed keeps their own permanent copy regardless of what you do afterward. This is exactly why CISA’s response to its leak was not to simply delete the file: it was to revoke and reissue every credential that had been exposed. Once a secret has been pushed anywhere public, the only safe assumption is that it is compromised, and rotation (not deletion) is the real fix. Catching the secret before that first commit is what this tutorial is about.
What you will need
- A Linux machine with a bash shell. These steps were verified on Ubuntu 24.04 LTS and also work on Ubuntu 22.04 LTS and Debian-based distributions; macOS users can substitute
brew install gitleaksfor the manual download in Step 1 and follow the rest unchanged. gitinstalled, and enough familiarity with it to rungit addandgit commit.curlandsha256sum(both preinstalled on virtually every Linux distribution).- Python 3.10 or newer with
pip, needed only for the pre-commit framework in Step 5. Ubuntu 22.04 and 24.04 both ship a new enough Python 3 by default. sudoaccess on the machine, to install the Gitleaks binary to a system directory.- No account or sign-up with any third-party service is required for the core workflow. Gitleaks runs entirely on your own machine.
Step 1: Install Gitleaks
Download and verify the binary
Gitleaks ships as a single self-contained binary for each platform, with no runtime dependencies to install. At the time of writing, the latest stable release is v8.30.1. Download the Linux x64 build together with its checksum file, and verify the download before running anything from it:
# Context: Ubuntu 22.04/24.04 LTS or similar, x86_64, in an empty scratch directory.
# Purpose: download Gitleaks v8.30.1 and its published checksums.
curl -sSLO https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz
curl -sSLO https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_checksums.txt
sha256sum --ignore-missing -c gitleaks_8.30.1_checksums.txt
Expected output: gitleaks_8.30.1_linux_x64.tar.gz: OK. The checksums file lists hashes for every platform’s archive, and --ignore-missing tells sha256sum to check only the file you actually downloaded rather than complain about the others being absent. If you ever see FAILED here, stop and re-download; it means the file does not match what the Gitleaks maintainers actually published, and you should not run it.
Install it and confirm the version
# Context: same directory, immediately after the checksum verified as OK.
# Purpose: extract the binary and install it to a directory on PATH.
tar -xzf gitleaks_8.30.1_linux_x64.tar.gz gitleaks
sudo install -m 755 gitleaks /usr/local/bin/gitleaks
gitleaks version
Expected output: 8.30.1. If your shell instead reports command not found, confirm /usr/local/bin is on your PATH with echo $PATH, or open a new terminal window.
Step 2: Run your first scan
Move into any git repository you own (an existing project is fine; nothing here modifies your files) and run Gitleaks’ git-history scanner:
# Context: inside the root directory of a real git repository you maintain.
# Purpose: scan every commit in the repository's history for secrets.
gitleaks git -v
Expected output on a clean repository looks like this (Gitleaks also prints a small ASCII logo banner above these lines, trimmed here for readability):
INF 1 commits scanned.
INF scanned ~148 bytes (148 bytes) in 137ms
INF no leaks found
The exact byte count, commit count, and timing will differ based on your own repository’s size and history; what matters is the final no leaks found line and an exit code of 0 (check it with echo $? right after the command). If Gitleaks does find something, it prints one block per finding instead, followed by a summary that says leaks found: N at warning level, and the command exits with status 1. A real finding looks like this:
Finding: github_token = "REDACTED"
Secret: REDACTED
RuleID: generic-api-key
Entropy: 5.297080
File: secrets_test.py
Line: 2
Commit: 405666cffb4de8acbcb44bb50d0ad1757d15f5ac
Author: Demo User
Email: [email protected]
Date: 2026-07-11T18:53:25Z
Fingerprint: 405666cffb4de8acbcb44bb50d0ad1757d15f5ac:secrets_test.py:generic-api-key:2
Explanation: RuleID tells you which detection rule matched (here, a generic high-entropy key rather than a named service like AWS or Stripe); Entropy is the randomness score described earlier; Commit, Author, and Date tell you exactly when and by whom the secret was introduced, which is invaluable when you need to figure out whether it is still in use; and Fingerprint is a stable identifier for this specific finding that you will reuse in Step 4. The example above was captured with --redact so no secret-shaped string appears on this page; by default Gitleaks does not redact anything, and the Secret field shows the real value, so add --redact yourself any time you plan to paste scan output somewhere else (a bug report, a Slack message, a CI log).
Step 3: Decide what a finding means
A finding is not automatically a five-alarm fire, but it always deserves a real answer to one question: is this credential still valid? If yes, treat it exactly like CISA treated its leak: rotate or revoke it immediately at the provider (AWS, Stripe, GitHub, whichever service issued it), then remove it from the current code and replace it with a reference to an environment variable or secret manager. Removing the line from your file is necessary for hygiene, but it is not the fix; the old value is already permanently readable in your git history, so revocation is what actually neutralizes the leak. If the flagged string is not a real secret at all (a test fixture, a documentation example, an expired key you already rotated), move on to Step 4 and teach Gitleaks to stop flagging that specific instance.
Step 4: Silence false positives without disabling the rule
Turning off a detection rule entirely to fix one false positive throws away real protection. Gitleaks gives you two more targeted options instead.
Option A: an inline allow comment
For a one-off case in a file you control, append #gitleaks:allow to the exact line:
# Context: any source file, appended to the specific line you want Gitleaks to skip.
# Purpose: mark one specific line as reviewed and intentionally allowed.
test_key = "AIzaSyD-9tSrke72PouQMnMX-a7eZSW0jkFMBWY" #gitleaks:allow
Re-running gitleaks git -v will no longer report that line, while every other finding still shows up normally. This was verified directly: adding the comment to a test file dropped that file’s finding out of the scan results without affecting any other finding in the same commit.
Option B: a .gitleaksignore file
For findings in files you cannot annotate (generated files, vendored code, or simply a team preference to keep suppressions in one place for reviewers to see), create a .gitleaksignore file at your repository root and paste in the exact Fingerprint value from the finding:
# Context: repository root, new file named exactly .gitleaksignore
# Purpose: suppress one specific, already-reviewed finding by its fingerprint.
405666cffb4de8acbcb44bb50d0ad1757d15f5ac:secrets_test.py:generic-api-key:2
This is the same format as the Fingerprint field shown in Step 2’s output: commit:file:rule-id:line. One fingerprint per line. Commit this file to version control like any other; that way, when a reviewer sees a suppressed finding was intentional, they can see exactly why in the diff instead of wondering whether someone quietly disabled scanning.
Step 5: Install the pre-commit framework
pre-commit is a general-purpose framework for running checks automatically every time you run git commit, before the commit is actually created. It manages the checks (called hooks) for you: installing each one in its own isolated environment and running only against the files you are about to commit. The current release is 4.6.0 and requires Python 3.10 or newer.
# Context: any machine with Python 3.10+ and pip.
# Purpose: install the pre-commit command-line tool.
pip install pre-commit
pre-commit --version
Expected output: a version string such as pre-commit 4.6.0. Installing pre-commit does nothing to any repository by itself; it just makes the pre-commit command available. The next step is what actually activates it for a specific project.
Step 6: Block leaks automatically with a pre-commit hook
Write the configuration file
Inside the git repository you want to protect, create a file named exactly .pre-commit-config.yaml at the repository root:
# Context: repository root, new file named exactly .pre-commit-config.yaml
# Purpose: tell pre-commit to run the official Gitleaks hook before every commit.
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
The rev field pins an exact, immutable tag rather than a branch name; pre-commit requires this so that every developer on your team, and every CI run, uses the identical, reproducible version of the hook instead of whatever happens to be newest on a given day. Run pre-commit autoupdate later whenever you deliberately want to bump it.
Activate the hook
# Context: inside the same repository, after saving .pre-commit-config.yaml.
# Purpose: register pre-commit's script as this repository's actual git commit hook.
pre-commit install
Expected output: pre-commit installed at .git/hooks/pre-commit. This step only has to be run once per local clone (it is not stored in git itself, so a teammate cloning the repository fresh needs to run it too). The first time the hook actually fires, pre-commit will spend a minute or two downloading and setting up Gitleaks in an isolated environment; every run after that reuses it instantly.
Confirm it actually blocks a leak
Add a file containing a fake but realistic-looking secret and try to commit it:
# Context: same repository, hook already installed.
# Purpose: prove the hook stops a commit that contains a secret.
echo 'stripe_key = "sk_live_51H8anLkj3nnfHZaXzZQnKl2C4dP9vX2Ab3Yc4Ef5Gh6Ij7Kl8Mn9"' > new_leak.py
git add new_leak.py
git commit -m "Add new leak test"
Expected output (verified directly): the commit is refused.
Detect hardcoded secrets.................................................Failed
- hook id: gitleaks
- exit code: 1
Finding: stripe_key = "REDACTED"
Secret: REDACTED
RuleID: stripe-access-token
Entropy: 5.137876
File: new_leak.py
Line: 1
Fingerprint: new_leak.py:stripe-access-token:1
INF 0 commits scanned.
INF scanned ~77 bytes (77 bytes) in 35.3ms
WRN leaks found: 1
A git log right after this confirms the commit was never created; your repository’s history is untouched. Notice the hook redacts the secret value by default, unlike the plain gitleaks git -v command in Step 2, and that the fingerprint here has no commit hash prefix because it is scanning your staged, not-yet-committed changes rather than existing history.
Confirm a clean commit still works
Fix the file so it loads the key from the environment instead of hardcoding it, then commit again:
# Context: same repository, same file.
# Purpose: prove the hook allows a commit through once the secret is gone.
cat > new_leak.py <<'EOF'
import os
stripe_key = os.environ["STRIPE_API_KEY"]
EOF
git add new_leak.py
git commit -m "Load Stripe key from environment instead"
Expected output: Detect hardcoded secrets.................................................Passed, immediately followed by git’s normal commit confirmation. The commit succeeds because there is no longer a secret in the diff for Gitleaks to find.
Step 7 (optional): add a second layer in GitHub Actions
A local hook only protects commits made on machines where it is installed. Anyone who clones the repository without running pre-commit install, or who deliberately bypasses it, still has a way to push a secret through: git’s own --no-verify flag skips every hook including this one, and SKIP=gitleaks git commit (verified directly while writing this tutorial) skips just this one. Either way, the local hook is a courtesy, not a wall. A CI check closes that gap, because it runs on GitHub’s servers regardless of what happened locally. Add this workflow file, which uses the official gitleaks-action:
# Context: repository root, new file at .github/workflows/gitleaks.yml
# Purpose: scan every push and pull request in GitHub Actions using gitleaks-action v3.
name: gitleaks
on:
pull_request:
push:
workflow_dispatch:
jobs:
scan:
name: gitleaks
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 }}
Explanation: fetch-depth: 0 is not optional here. GitHub’s checkout action fetches only the single latest commit by default, so without this setting gitleaks-action would only ever see one commit’s worth of history, silently missing everything else. GITHUB_TOKEN is provided automatically by GitHub Actions for every workflow run. GITLEAKS_LICENSE is only required if this repository belongs to a GitHub organization account; for a personal account repository, you can remove that line entirely. Once this workflow file is merged, every future pull request gets an automatic Gitleaks check alongside your other CI checks, and it cannot be skipped from a contributor’s local machine the way the pre-commit hook can.
Common mistakes and gotchas
- Treating file deletion as the fix. Removing a secret from your current files does nothing to the earlier commits that still contain it, and does nothing for anyone who already cloned or forked the repository. Rotate the credential at the source. This is the single most important lesson from the CISA incident that opened this tutorial.
- Trusting the local hook alone. As shown in Step 6, both
git commit --no-verifyandSKIP=gitleaks git commitbypass the pre-commit hook entirely, and a commit made that way succeeds normally with the secret intact. Treat Step 7’s CI check as mandatory, not optional, for anything beyond a personal solo project. - Suppressing findings you have not actually investigated. A
.gitleaksignoreentry or#gitleaks:allowcomment should mean “a human reviewed this and confirmed it is safe,” not “this was annoying.” Get in the habit of checking theCommit,Author, andDatefields before suppressing anything. - Forgetting a fresh clone needs setup too.
pre-commit installonly affects the local.gitdirectory it was run in, which is never itself tracked by git. Every teammate who clones the repository needs to run it once, or rely entirely on the CI layer from Step 7 until they do. - Skipping
fetch-depth: 0in CI. Covered in Step 7, but easy to miss if you copy a checkout step from an existing workflow that only needed the latest commit for a build or test job.
How to verify everything works end to end
- From your repository root, run
gitleaks git -vand confirm it ends withno leaks found(or only findings you have already reviewed and suppressed), and thatecho $?immediately afterward prints0. - Create a throwaway file with an obviously fake but realistic-looking token,
git addit, and attempt a commit. Confirm you seeDetect hardcoded secrets.................................................Failedand thatgit logshows the commit was never created. - Delete the fake token from the file, stage it again, and commit. Confirm you now see
...Passedand a normal commit succeeds. - If you completed Step 7, open a test pull request that includes a fake secret and confirm the “gitleaks” check fails on the pull request in GitHub’s UI, then push a follow-up commit removing it and confirm the check turns green.
Next steps
If working through this tutorial turned up a real, currently-valid secret in one of your own repositories, stop and rotate it at the provider right now, the same first move CISA made once it learned of its own leak. Beyond that immediate fix, point gitleaks git -v at your organization’s older repositories too, not just new ones; a project that predates this workflow can easily have years of unscanned history. For continuous, organization-wide monitoring rather than a repository-by-repository scan, look into GitHub’s own native secret scanning and push protection, and at GitGuardian’s ggshield, made by the same company whose researcher caught the original CISA leak, as a hosted alternative to running Gitleaks yourself. Longest term, the most durable fix is to stop hardcoding credentials at all: load them from environment variables backed by your platform’s secret manager, or a dedicated tool such as HashiCorp Vault, so there is nothing sitting in your source code for a scanner, or an attacker, to ever find.








No Comment! Be the first one.