PROJECTBeginner

Gitleaks: Stopping Leaked Secrets with Pre-Commit Hooks and CI Enforcement

Build a two-layer secret scanning pipeline with Gitleaks: a pre-commit hook that blocks credentials before they land in history, and a CI gate that catches anything the hook missed.

Dylan H.

Projects

November 11, 2026
8 min read
1-2 hours
Gitleaks: Stopping Leaked Secrets with Pre-Commit Hooks and CI Enforcement

Tools & Technologies

Gitleakspre-commitGitHub ActionsGit

Overview

Leaked credentials are one of the most common and most avoidable causes of breaches. An API key pasted into a config file, a .env committed by accident, a cloud token left in a test fixture: once it is pushed, it lives in git history, in forks, and in clones. Deleting the file in a later commit does not help, because the secret is still in the history and must be treated as compromised.

Gitleaks is an open-source scanner that finds secrets in git repositories, directories, and stdin using regex rules and entropy checks. In this project you will build a practical two-layer defense:

  1. Layer 1, pre-commit hook: scans staged changes on the developer machine and blocks the commit.
  2. Layer 2, CI gate: scans the full history on every push and pull request, because hooks can be skipped with --no-verify or never installed.

You will also learn to tune a .gitleaks.toml to cut false positives, adopt Gitleaks on a repo that already has historical findings by using a baseline, and respond when a real secret does leak.

Architecture and Design

 developer machine                      GitHub
┌──────────────────────┐         ┌────────────────────────────┐
│ git commit           │         │ push / pull_request        │
│   └─ pre-commit hook │  push   │   └─ Actions: gitleaks     │
│      gitleaks staged ├────────▶│      full-history scan     │
│      (blocks on hit) │         │      SARIF + fail the job  │
└──────────────────────┘         └────────────────────────────┘
          ▲                                   ▲
          └──────── shared .gitleaks.toml ────┘

Design decisions:

  • One config, two enforcement points. The same .gitleaks.toml is used locally and in CI, so a commit that passes the hook will not surprise you in CI.
  • Fast locally, thorough in CI. The hook scans only staged changes (milliseconds). CI scans the whole history with fetch-depth: 0.
  • Allowlist narrowly. Suppress by path, regex, or specific commit fingerprint, never by disabling rules globally.
  • Redact output. Findings are printed with --redact so CI logs do not become a second leak.

Prerequisites

  • A git repository (a throwaway test repo is ideal)
  • Python 3 for the pre-commit framework, or Docker if you prefer containers
  • A GitHub repository with Actions enabled for the CI portion

Step 1: Install Gitleaks and Run a First Scan

Install the binary (use your package manager or download a release from GitHub):

# macOS
brew install gitleaks
 
# Debian/Ubuntu: download the release tarball for your arch
# https://github.com/gitleaks/gitleaks/releases
# or use the container image, no install needed:
docker run --rm -v "$PWD:/repo" ghcr.io/gitleaks/gitleaks:latest \
  git /repo --redact -v

Scan an existing repo's full history:

gitleaks git . --redact -v --report-path /tmp/gitleaks-report.json

Note: older guides use gitleaks detect and gitleaks protect. These still work in current releases but are deprecated aliases of gitleaks git (history, or --staged for staged changes) and gitleaks dir (plain directories). Check gitleaks --help for your installed version.

Each finding reports the rule, file, line, commit, author, and a fingerprint (commit:file:rule:line) that you will use for suppression later.

Step 2: Create a Project Config

Create .gitleaks.toml in the repo root. Extend the default ruleset so you keep every built-in detector, then add your own rules and allowlist:

title = "Project gitleaks config"
 
[extend]
useDefault = true
 
# Custom rule: internal service tokens look like svc_<32 hex chars>
[[rules]]
id = "internal-service-token"
description = "Internal service token"
regex = '''\bsvc_[a-f0-9]{32}\b'''
keywords = ["svc_"]
 
# Narrow allowlist for known-safe content
[allowlist]
description = "Global allowlist"
paths = [
  '''(^|/)tests?/fixtures/''',
  '''(^|/)docs/examples/''',
  '''\.lock$''',
  '''package-lock\.json$''',
]
regexes = [
  '''EXAMPLE_KEY_DO_NOT_USE''',
  '''AKIAIOSFODNN7EXAMPLE''',
]

AKIAIOSFODNN7EXAMPLE is the documented placeholder AWS access key and is safe to allowlist. The keywords field is a speed optimization: Gitleaks only evaluates the regex on lines containing a keyword.

Step 3: Add the Pre-Commit Hook

Install the framework and create .pre-commit-config.yaml:

pip install pre-commit
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.0          # pin to a current release tag
    hooks:
      - id: gitleaks
        args: ["--config=.gitleaks.toml", "--redact"]

Activate it:

pre-commit install
pre-commit autoupdate   # bumps rev to the latest tag
pre-commit run --all-files

The hook now runs on every git commit. Commit both files so the whole team shares the rules. Pin rev to a tag rather than main so a surprise upstream change cannot break your commits, and let Renovate or pre-commit autoupdate handle bumps.

Step 4: Add the CI Gate

Create .github/workflows/secret-scan.yml:

name: Secret Scan
 
on:
  pull_request:
  push:
    branches: [main]
 
permissions:
  contents: read
  security-events: write   # for SARIF upload
 
jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # full history, required for a real scan
 
      - name: Install gitleaks
        run: |
          VER=8.30.0
          curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" \
            | tar -xz gitleaks
          sudo mv gitleaks /usr/local/bin/
 
      - name: Scan history
        run: |
          gitleaks git . \
            --config .gitleaks.toml \
            --redact -v \
            --report-format sarif \
            --report-path gitleaks.sarif
 
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: gitleaks.sarif

Running the binary directly avoids the license requirement that the packaged gitleaks-action imposes on organization-owned repos, and it keeps the exact same command as your local run. Gitleaks exits with code 1 on findings, which fails the job. Mark this check as required under branch protection so it cannot be merged around.

Step 5: Adopt on a Repo with Existing Findings

A legacy repo will light up with historical findings, and blocking on all of them makes the tool unusable. Use a baseline: record today's findings and only fail on new ones.

gitleaks git . --report-format json --report-path .gitleaks-baseline.json
git add .gitleaks-baseline.json

Then run with the baseline in both the hook args and CI:

gitleaks git . --config .gitleaks.toml --baseline-path .gitleaks-baseline.json --redact

Important caveat: a baseline suppresses findings, it does not fix them. Every secret in the baseline that was real must still be rotated. Track that as a separate task and shrink the baseline over time.

For one-off false positives, add the fingerprint to a .gitleaksignore file in the repo root, one per line, ideally with a comment:

# Test token used in unit tests, not valid anywhere
3f9a1c2d...:tests/auth.test.ts:generic-api-key:42

An inline # gitleaks:allow comment on the offending line works for single lines in code.

Testing

Prove each layer works with a harmless fake secret. Use a throwaway branch.

git checkout -b test/secret-scan
echo 'github_token = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"' > leak.py
git add leak.py
git commit -m "test: leak"

Expected: the hook fails, prints the rule (for example github-pat), the file and line, and the secret as REDACTED. The commit is not created.

Now bypass the hook to simulate a developer who skipped it, then push:

git commit --no-verify -m "test: leak (bypass)"
git push -u origin test/secret-scan

Open a pull request. The Secret Scan job should fail and the findings should appear in the Security tab via SARIF. Also test your custom rule with a svc_ token of 32 hex characters, and verify an allowlisted path such as tests/fixtures/ does not trigger. Delete the test branch afterwards; if you pushed it, remember the fake value is in that history.

Deployment

Roll this out across many repos without hand-copying files:

  • Template repo or org-level reusable workflow. Put the workflow in a central .github repo and call it with uses: org/.github/.github/workflows/secret-scan.yml@main.
  • Shared config. Keep a canonical .gitleaks.toml in the central repo and have the workflow fetch it, or use the [extend] path = option so local files add to a base.
  • Bootstrap script for developers:
#!/usr/bin/env bash
set -euo pipefail
pip install --user pre-commit
pre-commit install
pre-commit run --all-files
  • Add GitHub push protection as a third layer. It blocks known provider token patterns server-side at push time and is the only layer a developer cannot bypass locally. Gitleaks complements it by catching custom and generic secrets that provider patterns do not know about.
  • Keep it pinned and updated. Use Renovate to bump the hook rev and the CI version together.

Incident Response: When a Real Secret Leaks

If CI flags a genuine credential:

  1. Revoke and rotate immediately. This is the only step that actually fixes the exposure. Assume it was already harvested.
  2. Check the provider's audit log for use of the key between commit time and rotation.
  3. Optionally rewrite history with git filter-repo to remove it, but only after rotating. Forks and caches may still hold it.
  4. Add a rule or allowlist change only if the detection exposed a gap, not to silence a real finding.

Extensions and Next Steps

  • Add TruffleHog in CI with verification enabled, which tests whether detected credentials are still live and cuts triage time.
  • Scan container images and build contexts with gitleaks dir before publishing.
  • Feed SARIF or JSON findings into DefectDojo for tracking and metrics.
  • Write more custom rules for your own token formats and measure false-positive rate monthly.
  • Replace long-lived secrets with short-lived credentials (OIDC federation for CI, a secrets manager such as Vault) so a leak has a small blast radius.
  • Add a scheduled weekly full-history scan to catch rules updated after a commit landed.

Sources