I do not like discovering secrets in CI/CD.
Not because CI/CD secret scanning is useless. It is useful. It is necessary. I still want it in every serious pipeline.
The problem is timing.
When a token, password, private key, or API credential is detected in CI/CD, the code has already left the developer machine. It may already exist in a branch, a merge request, remote Git history, build logs, pipeline artifacts, caches, forks, or someone else's local clone. At that point the task is no longer just "remove the line from the code". The task becomes rotation, cleanup, investigation, and explaining why a preventable mistake reached shared infrastructure.
That is the part I wanted to remove from the normal development loop.
For secrets, the best finding is the one that appears before the commit exists.
The problem I kept running into
Most teams already have security tools. They have SAST. They have dependency scanning. They have CI/CD jobs. They have repository rules. They may even have secret detection enabled at the platform level.
But the first feedback often still appears after a push.
That means the developer workflow looks like this:
write code
commit
push
wait for pipeline
pipeline fails
open logs
find secret warning
fix locally
push again
maybe rotate credential
maybe clean history
This is not a great experience for developers, and it is not a great control for security teams.
A developer made the mistake locally, but the warning appears remotely. The context switch is unnecessary. The delay is unnecessary. The risk is unnecessary.
The uncomfortable truth is that CI/CD is often used as the first security feedback loop, when it should be the shared verification layer.
Why CI-only secret scanning is too late
A dependency vulnerability and a leaked secret are not the same class of problem.
If a library has a CVE, I can usually upgrade it, add a temporary mitigation, or accept the risk while the team plans a fix.
If a real credential lands in Git, I have to assume exposure. Even if the branch is private. Even if the commit is reverted. Even if the file was visible for only a few minutes. Git history and pipeline systems are not designed around pretending a committed secret never existed.
This is why I treat secrets differently.
For secrets, I want the first line of defense here:
before commit -> before push -> before CI/CD -> before review
Not because local checks are perfect. They are not. A developer can forget to install hooks. A hook can be bypassed. A scanner can miss something.
But a local hook catches the boring, obvious, expensive mistakes before they become shared problems.
That is already a win.
The principle: security checks should run where the mistake happens
The developer writes the code locally. The developer stages the change locally. The developer creates the commit locally.
So a part of the security feedback should also happen locally.
This matters for two reasons.
First, the code does not need to leave the machine just to catch obvious mistakes. A local scanner can inspect staged changes without sending source code to a remote service.
Second, the feedback is immediate. The developer does not need to wait for a pipeline, open a web UI, read a long job log, and then mentally reconnect the finding back to the line they just wrote.
The better loop looks like this:
write code
stage changes
try to commit
secret detected locally
fix it immediately
commit clean code
That is the kind of security control developers can actually live with.
The local layer: pre-commit plus Gitleaks
For this layer I use two pieces:
pre-commit -> manages Git hooks
Gitleaks -> scans for secrets
pre-commit gives the repository a shared way to define hooks. Instead of asking every developer to manually create scripts inside .git/hooks, the project keeps one versioned configuration file:
.pre-commit-config.yaml
Gitleaks does the secret scanning. In this setup I care about one specific behavior: run the scan before Git creates the commit.
A minimal configuration looks like this:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
Then the developer installs the hook once:
pip install pre-commit
pre-commit install
Before trusting it on an existing repository, I usually run the hook across the current tree:
pre-commit run --all-files
After that, the normal flow stays normal:
git add .
git commit -m "Implement payment validation"
The important part is what happens between git commit and the commit actually being created. The hook runs. Gitleaks scans the staged change. If it sees a likely secret, the commit stops locally.
No branch. No merge request. No pipeline artifact. No shared Git history.
Just a local warning at the point where the developer can fix it fastest.
I do not want this to become a README
The value is not the YAML snippet.
The value is the placement of the control.
A pre-commit hook is not a security program. It is not a replacement for CI/CD scanning, repository protection, secret management, review, or credential rotation. It is one small local safety net.
But it changes the tone of the workflow.
Instead of security saying:
You pushed a secret. Now go clean it up.
The tool says:
This looks like a secret.
Do not commit it.
Move it somewhere safe.
Try again.
That is a completely different developer experience.
Project-specific Gitleaks configuration
Default rules are useful, but real projects usually need a little tuning.
Test fixtures, documentation examples, generated samples, and fake tokens can produce false positives. I do not want developers fighting the tool every day, but I also do not want a giant allowlist that hides real problems.
A small .gitleaks.toml is usually enough:
title = "Project Gitleaks Configuration"
[extend]
useDefault = true
[[allowlists]]
description = "Allow fake examples in documentation and test fixtures"
paths = [
'''docs/examples/.*''',
'''src/test/resources/.*'''
]
[[allowlists]]
description = "Allow clearly fake placeholder values"
regexTarget = "line"
regexes = [
'''not-a-real-secret''',
'''example-token''',
'''dummy-password'''
]
My rule for allowlists is simple: they should explain why the value is safe.
If the answer is "because it is annoying", that is not a good allowlist. If the answer is "because this directory contains fake examples used in tests", that is much better.
When Gitleaks reports something, the first question should be:
Is this a real credential?
Not:
How do I silence the scanner?
If it is real, the fix is to remove it from code, move it to environment variables or a secret manager, rotate it if needed, and check whether it reached remote history.
The allowlist is for false positives. It is not a trash bin for uncomfortable findings.
Where pre-commit ends and build tooling begins
I do not put every security check into pre-commit.
That is how good controls become hated controls.
A commit hook should be fast enough that developers do not look for ways around it. Secret scanning is a good fit because the impact is high and the check can be quick. Formatting, simple linting, YAML/JSON validation, and accidental large-file detection also fit well.
Heavier checks belong somewhere else.
The way I model it is:
pre-commit
fast checks: secrets, formatting, simple file validation
pre-push
medium checks: broader scans, policy validation, selected tests
build tooling layer
full local project checks: SCA, SBOM, coverage, SonarQube metadata
CI/CD
clean shared execution, gates, artifacts, enforcement
This is where my Java secure build tooling fits in.
For Gradle projects, I want a command like:
./gradlew clean securityAnalyze --no-daemon
For Maven projects, I want the normal lifecycle to carry the security workflow:
mvn verify
That build layer can run Dependency-Check, generate a CycloneDX SBOM, prepare coverage, and configure SonarQube metadata. It is heavier than a pre-commit hook, but it is still local-friendly and CI/CD-ready.
The key is that each layer has a job.
Gitleaks + pre-commit
catches secrets before commit
Gradle/Maven build tooling
runs repeatable AppSec checks locally and in CI/CD
CI/CD
verifies the same workflow in a clean shared environment
This keeps security close to the developer without turning every commit into a full compliance audit.
CI/CD still stays in the design
Local hooks are not enough.
Someone can skip hook installation. A machine can be misconfigured. Automation can push code. A developer can bypass a hook intentionally. That is why I still run Gitleaks in CI/CD.
The design principle is not "local instead of CI".
The principle is:
local for fast feedback
CI/CD for enforcement
A simple CI job can use the same idea:
secret_scan:gitleaks:
image:
name: zricethezav/gitleaks:v8.24.2
entrypoint: [""]
stage: test
script:
- gitleaks git --redact --verbose
For direct local scans outside pre-commit, I prefer the current Gitleaks command style:
gitleaks git --redact --verbose
gitleaks dir --redact --verbose .
This gives security teams a shared safety net while still giving developers a faster local warning.
What developers get
Developers do not need another portal to check before every commit.
They need a clear signal in the workflow they already use.
A good local secret scanning setup gives them:
- feedback before the mistake becomes Git history;
- no need to upload code to catch obvious secrets;
- fewer surprise pipeline failures;
- a small, repeatable command set;
- less back-and-forth with security for simple mistakes.
The best part is that the fix usually happens while the developer still remembers what they changed.
That matters more than people admit.
What the security team gets
Security teams get fewer noisy incidents and a better place to enforce basic hygiene.
Instead of treating every obvious secret as a pipeline incident, the team can push the simple feedback left and reserve CI/CD for shared verification.
They also get a cleaner story for Secure SDLC:
before commit: local secret scanning
before push: optional broader hooks
build: SCA, SBOM, coverage, SonarQube metadata
CI/CD: artifacts, gates, audit trail
That is much easier to explain, maintain, and improve than a pile of disconnected scanner jobs.
The part that matters
This is not really about Gitleaks.
Gitleaks is the tool I use under the hood. The more important idea is that security checks should be placed where they reduce cost the most.
For secrets, that place is before the commit.
For dependency analysis and SBOM generation, that place is usually the build tooling layer, with the same behavior available locally and in CI/CD.
For enforcement, that place is CI/CD.
When those layers work together, security stops feeling like an external inspection step and starts feeling like normal engineering feedback.
That is the workflow I want: fast locally, reproducible in CI/CD, and boring in the best possible way.
Top comments (0)