gitleaks CI Setup: Secret Detection in 10 Minutes
A hardcoded secret doesn't wait for your next security review. The moment the commit lands, the key, token, or password is in your git history, and if that repo is public, bots are scraping for exactly that pattern within minutes. Rotating it afterward is damage control, not prevention.
gitleaks closes that window. It's a free, open-source scanner a single Go binary that reads your git history for secrets and fails the build the moment it finds one. Wiring it into your pipeline takes under ten minutes. This guide covers the setup for GitHub Actions, GitLab CI, and Jenkins, what to do the first time it fires, how to keep false positives from becoming noise, and why running it on every commit beats running it once before deployment.
Why every commit beats scanning before deployment
The instinct is to add a security scan as a gate right before you ship. For secrets, that's already too late. A credential committed on Monday and “removed” on Tuesday is still sitting in Monday's commit and a pre-deployment scan that only looks at the current state of the code never sees it.
Scanning on every commit and every pull request moves detection to the moment the secret is introduced. You catch it in the PR that added it, while the author is right there and the context is fresh, and the merge is blocked before the secret ever reaches your main branch. That's the difference between a two-minute fix in review and an incident response weeks later. Secrets leak at commit time, so that's where the gate belongs.
The under-10-minute setup: GitHub Actions
Drop this file at .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
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Organization repos also need a free license key:
# GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
Three details matter. fetch-depth: 0 pulls the full history — a shallow clone hides older commits where a secret may already be sitting. GITHUB_TOKEN is provided automatically and lets the action comment findings directly on the PR. And use @v3: the v2 action stops working after 16 September 2026, when GitHub removes Node 20 from its runners.
The licensing detail most tutorials skip
Since v2.0.0, the official gitleaks-action is no longer MIT-licensed. Personal repos need no key. Organization-owned repos need a GITLEAKS_LICENSE a free “Starter” tier covers one repo, with a paid license beyond that. If you'd rather not manage a license at all, skip the action and call the CLI directly; the gitleaks binary itself stays open source and free. That approach also happens to be identical to what you'll use for GitLab and Jenkins below.
GitLab CI
GitLab ships a native Secret Detection template built on gitleaks, but running gitleaks yourself keeps the config identical across platforms. Add this to .gitlab-ci.yml:
secret_scan:
stage: test
image:
name: ghcr.io/gitleaks/gitleaks:latest
entrypoint: [""]
variables:
GIT_DEPTH: 0 # full history, not a shallow clone
script:
- gitleaks git . --redact -v --exit-code 1
GitLab shallow-clones by default, so GIT_DEPTH: 0 matters here for the same reason fetch-depth: 0 did on GitHub. --redact keeps the actual secret out of your CI logs, and --exit-code 1 fails the job on any finding.
Jenkins
On Jenkins, the most portable option is to run the gitleaks container from a declarative pipeline — no agent-level install required:
pipeline {
agent any
stages {
stage('Secret Scan') {
steps {
sh '''
docker run --rm -v "$WORKSPACE":/repo \
ghcr.io/gitleaks/gitleaks:latest \
git /repo --redact -v --exit-code 1
'''
}
}
}
}
If gitleaks is already installed on the agent, drop the Docker wrapper and call gitleaks git . --redact -v --exit-code 1 directly. Either way, make sure the job checks out full history rather than a shallow clone, or older commits won't be scanned. A non-zero exit fails the stage, which is exactly the gate you want.
One note on commands: recent gitleaks releases use gitleaks git and gitleaks dir; older tutorials show gitleaks detect, which still works as an alias. If you're pinning a version, pin the image tag too so behaviour doesn't drift.
Tuning it so it doesn't cry wolf
The first run on an older repo often surfaces test fixtures, example configs, and long-dead placeholder values. That's normal. A scanner you've muted with blanket exclusions is worse than no scanner, so tune deliberately:
Custom rules and allowlists live in a .gitleaks.toml at the repo root (or point GITLEAKS_CONFIG at one). Extend the default ruleset rather than replacing it, so you keep the built-in detections.
Known false positives go in .gitleaksignore paste the fingerprint (commit:file:rule:line) that the report gives you. This ignores one specific finding, not an entire rule or path.
Prefer narrow allowlist entries (a specific path or regex) over disabling a rule globally. A disabled rule is a blind spot that never comes back.
Re-run after tuning and confirm the job goes green for the right reasons zero findings, not zero rules.
What to do the first time it fires
When gitleaks flags a real secret, the order of operations matters:
Rotate first. The credential is compromised the moment it's public revoke and reissue it before anything else. Cleaning history on a key that's still valid accomplishes nothing.
Then purge it from history with git filter-repo or BFG. Deleting the line in a new commit leaves the secret in every earlier commit, which is the whole reason the scanner caught it.
Force-push the rewritten history and have collaborators re-clone. Coordinate this a rewrite surprises teammates mid-branch.
Close the loop: move the value into a secrets manager or CI variable so the same mistake can't reappear on the next commit.
Where a single secrets scanner stops
gitleaks does one job well: it finds secret strings. It won't flag a vulnerable dependency, a misconfigured container, or an injection pattern in code you wrote yourself. That's why most teams end up running three scanners gitleaks for secrets, Trivy for dependency, container, and IaC vulnerabilities, and Semgrep for static code analysis. Each is strong at its own slice.
The catch is that you now have three reports, on three schedules, and none of them knows which requirement or pull request a finding belongs to. A hardcoded key, a vulnerable package, and a missing auth check might all trace back to the same rushed change often an AI-generated one but nothing connects them for you. WalnutAI's role sits exactly there: its analysis ties findings back to the story and PR that introduced them, so a security result is a traceable event in your SDLC instead of a line in a log nobody owns.



