Hardening Your Git Workflow: .gitignore, Gitleaks, and Pipeline Secret Scanning
Most credential leaks don’t happen because of hackers. They happen because a developer hit git push without thinking. Here's how to make…
Hardening Your Git Workflow: .gitignore, Gitleaks, and Pipeline Secret Scanning

Most credential leaks don’t happen because of hackers. They happen because a developer hit
git pushwithout thinking. Here's how to make that impossible.
Picture this: it’s 3 AM and nobody on your team is awake. But an automated bot is — and it just found an AWS key you committed six hours ago. By morning, you’re staring at a $47,000 cloud bill from hundreds of crypto-mining EC2 instances that spun up while you slept. This exact scenario played out in 2023, and the window between exposure and exploitation was just 11 minutes.
The frustrating part? Two tools sitting right in your development workflow would have stopped it cold: a thoughtfully configured .gitignore and Gitleaks running as a pre-commit hook — and as a CI/CD pipeline check.
Walk through this guide and you’ll have both operational — plus scanners running inside every major CI/CD platform you might use.
Git Doesn’t Forget — Even When You Want It To
Think of your Git repository less like a folder of files and more like a ledger that keeps every transaction forever. When you commit a file, that snapshot is permanent. When you delete it, you’re adding a new entry that says “this file is gone” — but every previous entry remains untouched and fully readable.
The moment a secret touches a commit, it’s out of your control. Removing the file afterward is cosmetic — it changes nothing about what’s already recorded.
This is why developers who discover a leaked credential and respond by simply deleting the file are still in trouble. Every person who cloned the repository, every service that mirrored it, every bot that indexed it — they already have a copy of that commit. The only real fix is preventing the commit from happening in the first place, then backing that prevention up at the pipeline level for anything that slips through.
🔒 First Principle
A credential that never reaches a commit cannot be leaked through version control. Build your defences at the earliest possible point — your local machine — and treat CI/CD scanning as the fallback, not the primary check.
Part 1: .gitignore as a Security Boundary
Most engineers treat .gitignore as a housekeeping file — somewhere to tuck away build output and IDE clutter. Flip that perspective. Treat it as an access control list for your repository. Everything that should never leave your machine belongs here, configured explicitly before you write your first line of code.
Files That Should Never Enter Version Control
- All environment variable files:
.env,.env.*,.env.local,.env.production - Credential stores:
credentials.json,secrets.yaml,config/secrets.yml - Cryptographic keys:
*.pem,*.key,id_rsa,id_ed25519 - Platform credential directories:
.aws/credentials,.gcloud/,kubeconfig - Application logs:
*.log— these often capture full HTTP headers, including bearer tokens - Local databases:
*.sqlite,*.db
⚠️ Timing Matters
*.gitignore only intercepts files that haven't been tracked yet. If Git is already watching a file, adding it to .gitignore does nothing. You'll need git rm --cached <file> to stop tracking it — and once you do, rotate whatever credentials were inside it, immediately.*
A Security-First .gitignore Template
# Environment & Secrets
.env
.env.*
!.env.example
*.local
# Private Keys
*.pem
*.key
*.p12
id_rsa
id_ed25519
# Cloud Credentials
.aws/
.gcloud/
kubeconfig
serviceAccountKey.json
# Secret Config Files
secrets.yaml
secrets.yml
credentials.json
config/secrets.*
# Logs
*.log
logs/
# Database
*.sqlite
*.sqlite3
*.db
# IDE & OS
.idea/
.vscode/
.DS_Store
Thumbs.db
Don’t Start from Scratch — Use gitignore.io
Rather than building your .gitignore by hand, gitignore.io (also reachable at gitignore.io) generates a tailored file based on your tech stack. Type in your languages, frameworks, editors, and operating systems — it outputs a comprehensive, community-maintained .gitignore covering all of them. It's a solid starting point that you then extend with the security-sensitive entries above.
Keeping New Developers Safe: The .env.example Pattern
When you exclude your .env file, contributors who clone the project have no idea what variables the application needs. Solve this by committing a .env.example alongside it — a file that documents every required variable using obviously fake placeholder values. Developers copy it locally, substitute real values, and the actual .env never touches Git.
# .env.example — COMMIT THIS FILE (no real secrets)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
AWS_ACCESS_KEY_ID=your_access_key_here
STRIPE_SECRET_KEY=sk_live_your_key_here
JWT_SECRET=your_jwt_secret_here
Part 2: Blocking Bad Commits Before They Happen
Scanning history tells you what’s already done. Pre-commit hooks stop problems before they start. A pre-commit hook is a script Git invokes automatically whenever you run git commit — and if the script exits with a non-zero code, the commit is cancelled entirely.
Option A: The Minimal Setup
For solo projects or quick onboarding, a single line in your hook file is all you need:
# .git/hooks/pre-commit
#!/bin/sh
gitleaks protect --staged --redact
Try committing a file that contains a real-looking secret and you’ll see something like this:
$ git commit -m "add config"
Finding: STRIPE_SECRET_KEY=sk_live_51HG...
RuleID: stripe-secret-key
File: src/config.js Line: 8
Detect hardcoded secrets.........................Failed
The commit never happens. The secret never leaves your machine.
Option B: The pre-commit Framework (Best for Teams)
Writing directly to .git/hooks/ has one major drawback — those files aren't committed to the repository, so each developer manages their own. Standards drift, and coverage becomes inconsistent across the team. The pre-commit framework solves this by centralising all hook definitions in a YAML file that does live in version control.
Step 1: Install the framework:
brew install pre-commit # macOS
pip install pre-commit # cross-platform
Step 2: Define your hooks in .pre-commit-config.yaml at the repository root:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
Step 3: Activate and verify:
pre-commit install
# pre-commit installed at .git/hooks/pre-commit
Now trigger a detection with a fake Stripe secret. Use the command for your platform:
macOS / Linux
echo 'STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1' > secret_test.py
git add secret_test.py && git commit -m "test"
Windows CMD
echo STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1 > secret_test.py
git add secret_test.py && git commit -m "test"
Windows PowerShell
Set-Content secret_test.py 'STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1'
git add secret_test.py && git commit -m "test"
Expected output — commit is blocked:
Detect hardcoded secrets................................................Failed
- hook id: gitleaks
- exit code: 1
Finding: STRIPE_SECRET_KEY=REDACTED
RuleID: stripe-access-token
Entropy: 4.875000
File: secret_test.py
Line: 1
Clean up the test file:
git restore secret_test.py # macOS / Linux
git checkout secret_test.py # Windows
⚠️ Why Not the AWS Example Key?
You may have seen tutorials use AWS_SECRET=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY as a test value. This will not trigger Gitleaks. That exact string — along with other well-known documentation keys like AKIAIOSFODNN7EXAMPLE and ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456789 — is on Gitleaks' internal allowlist because it appears in thousands of official AWS, GitHub, and tutorial pages. Always use a realistic-looking key with random characters that isn't from official documentation. The Stripe key above works reliably for this purpose.
✅ Commit
.pre-commit-config.yamlto the repository. Any developer who clones the project and runspre-commit installautomatically inherits the same protections — one command, consistent security posture across the whole team.
Part 3: Gitleaks — Your Automated Secret Detector
Even a well-maintained .gitignore won't catch a developer who hardcodes an API key directly into app.js because they were "just testing." That's where Gitleaks earns its place in the toolchain.
Gitleaks is an open-source secret scanner — MIT-licensed, with north of 19,000 GitHub stars and over 20 million Docker pulls. It scans your repository using a combination of regular expression matching and Shannon entropy analysis. Strings that look like real credentials (high randomness, pattern-matches a known format) get flagged. It ships with detection rules for 160+ credential types and lets you extend those rules for anything proprietary to your organisation.
💡 Under the Hood
Entropy scoring is what separates Gitleaks from a basic regex grep. Placeholder values like your_api_key_here score low on the entropy scale and pass through. High-entropy strings that match a known secret pattern — like sk_live_4xKqP9mN2rL7vB8wE3jH6tY1 (entropy: 4.875) — get caught. Note that well-known documentation example keys are internally allowlisted by Gitleaks regardless of their entropy, so always test with realistic random values rather than strings copied from official docs.
Getting Gitleaks Installed (Current Release: v8.30.0)
# macOS
brew install gitleaks
# Linux (Debian / Ubuntu)
sudo apt install gitleaks
# Linux (Red Hat / Fedora)
sudo dnf install gitleaks
# Windows (Chocolatey)
choco install gitleaks
# Confirm the installation
gitleaks version # Expected: v8.30.0
Auditing What’s Already in Your Repository
Before setting up ongoing protection, run a retrospective scan across your full commit history. You may already have leaked credentials sitting in old commits.
# Scan full git history
gitleaks git .
# Verbose output
gitleaks git . --verbose
# Save findings to a JSON report
gitleaks git . --report-path gitleaks-report.json
Here’s what a positive finding looks like:
Finding: STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1
RuleID: stripe-access-token
Entropy: 4.875
File: config/deploy.rb Line: 14
Commit: 3f2a1b9c...
Author: dev@example.com
Spot real credentials in that output? Rotate them before doing anything else — before cleaning history, before notifying anyone. Even visibility into a private repository is enough to constitute a breach.
Part 4: Tuning Gitleaks with .gitleaks.toml (Custom Rules)
Out of the box, Gitleaks is thorough — sometimes more thorough than you need. Documentation files, example configs, and test fixtures will generate false positives against the default ruleset. The .gitleaks.toml configuration file lets you suppress that noise while also adding detection rules tailored to tokens your platform uses internally.
# .gitleaks.toml
[extend]
useDefault = true
[[allowlists]]
description = "Ignore example values in documentation and templates"
paths = ['''docs/.*''', '''README.md''', '''.env.example''']
[[rules]]
description = "Proprietary Internal API Token"
id = "myapp-api-token"
regex = '''myapp_[0-9a-zA-Z]{32}'''
tags = ["api", "internal"]
The useDefault = true directive preserves all 160+ built-in rules while layering your customisations on top. The allowlist exempts specific paths where fake credentials are intentional by design, and the custom rule adds detection for your own token format — something the default ruleset naturally can't know about.
Part 5: Enforcing Secret Scanning Across Every CI/CD Platform
Local hooks protect developers who set them up — but what about someone who joined the team last week and hasn’t run pre-commit install yet? Or a CI bot making automated commits? Or a third-party integration pushing directly to a branch?
Developer workstation → pre-commit hook → stopped at source
↓ (if bypassed for any reason)
CI/CD pipeline → Gitleaks scan → PR or branch blocked
↓ (belt and suspenders)
Protected branch → clean history → nothing dangerous merged
GitHub Actions
The official gitleaks-action handles the integration end to end — no binary installation, no manual configuration. It scans every push and pull request and posts inline annotations when it finds something suspicious.
# .github/workflows/gitleaks.yml
name: Secret Scanning
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required — shallow clones miss historical secrets
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 is the one setting you cannot skip. GitHub Actions defaults to a shallow clone containing only the most recent commit. Without the full history, Gitleaks is blind to anything that leaked weeks or months ago.
GitLab CI/CD
GitLab pipelines use the official Gitleaks Docker image, keeping the setup clean and version-pinned without any installation steps on the runner.
# .gitlab-ci.yml
stages:
- security
- build
- test
- deploy
gitleaks:
stage: security
image: zricethezav/gitleaks:v8.30.0
script:
- gitleaks git . --verbose --report-path gitleaks-report.json
artifacts:
when: on_failure
paths:
- gitleaks-report.json
expire_in: 7 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
On large repositories where scanning the full history on every merge request is slow, scope the scan to commits introduced by the MR only:
gitleaks-diff:
stage: security
image: zricethezav/gitleaks:v8.30.0
script:
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- gitleaks git . --log-opts="origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}..HEAD" --verbose
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
This scans only the delta between the feature branch and the merge target — substantially faster for repositories with long commit histories.
Azure DevOps
Azure Pipelines requires downloading the Gitleaks binary directly since there’s no managed action equivalent. Pin the download URL to a specific version — Azure agents are ephemeral and rebuild from scratch on every run, so version consistency is your responsibility.
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
pr:
branches:
include:
- main
- develop
stages:
- stage: Security
displayName: Security Scanning
jobs:
- job: Gitleaks
displayName: Secret Scanning with Gitleaks
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
fetchDepth: 0 # Full repository history
- script: |
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.30.0/gitleaks_8.30.0_linux_x64.tar.gz | tar -xz
sudo mv gitleaks /usr/local/bin/
displayName: Install Gitleaks
- script: |
gitleaks git . --verbose --report-path $(Build.ArtifactStagingDirectory)/gitleaks-report.json
displayName: Run Gitleaks Scan
- task: PublishBuildArtifacts@1
condition: failed()
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)/gitleaks-report.json
artifactName: gitleaks-findings
displayName: Archive Findings on Failure
If your agent pool supports Docker, this alternative is cleaner and eliminates the install step entirely:
- task: Docker@2
displayName: Run Gitleaks via Docker
inputs:
command: run
arguments: >
-v $(Build.SourcesDirectory):/repo
zricethezav/gitleaks:v8.30.0
git /repo --verbose
Bitbucket Pipelines
Bitbucket’s architecture makes the Docker-as-image approach particularly clean. Each pipeline step runs inside its own isolated container, so pointing the step image directly at Gitleaks means zero setup overhead and guaranteed version consistency.
# bitbucket-pipelines.yml
image: atlassian/default-image:4
definitions:
steps:
- step: &gitleaks-scan
name: Secret Scanning (Gitleaks)
image: zricethezav/gitleaks:v8.30.0
script:
- gitleaks git . --verbose --report-path gitleaks-report.json
artifacts:
- gitleaks-report.json
pipelines:
default:
- step: *gitleaks-scan
branches:
main:
- step: *gitleaks-scan
- step:
name: Build & Test
script:
- echo "Build steps here"
pull-requests:
'**':
- step: *gitleaks-scan
To scan only commits introduced by a pull request rather than the full history:
- step:
name: Scan PR Commits Only
image: zricethezav/gitleaks:v8.30.0
script:
- git fetch origin $BITBUCKET_PR_DESTINATION_BRANCH
- gitleaks git . --log-opts="origin/${BITBUCKET_PR_DESTINATION_BRANCH}..HEAD" --verbose
Jenkins
Jenkins supports two integration patterns. The shell-based approach works on any agent type; the Docker agent is more reproducible across heterogeneous build infrastructure.
Shell-based (universal compatibility):
// Jenkinsfile
pipeline {
agent any
stages {
stage('Secret Scanning') {
steps {
sh '''
if ! command -v gitleaks &> /dev/null; then
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.30.0/gitleaks_8.30.0_linux_x64.tar.gz | tar -xz
sudo mv gitleaks /usr/local/bin/
fi
gitleaks git . --verbose --report-path gitleaks-report.json
'''
}
post {
failure {
archiveArtifacts artifacts: 'gitleaks-report.json', allowEmptyArchive: true
error 'Secret detected by Gitleaks — build halted.'
}
}
}
stage('Build') {
steps {
echo 'Build steps here'
}
}
}
}
Docker agent (recommended for consistent environments):
pipeline {
agent {
docker {
image 'zricethezav/gitleaks:v8.30.0'
args '-v $WORKSPACE:/repo --entrypoint=""'
}
}
stages {
stage('Secret Scanning') {
steps {
sh 'gitleaks git /repo --verbose --report-path /repo/gitleaks-report.json'
}
post {
failure {
archiveArtifacts artifacts: 'gitleaks-report.json'
}
}
}
}
}
One Jenkins-specific detail worth highlighting: the post { failure { } } block is essential. Without it, the findings report is cleaned up along with the failed build workspace and you lose visibility into exactly what triggered the block. If you manage multiple repositories, consider packaging the Gitleaks install logic into a Jenkins Shared Library so teams aren't each maintaining their own copy.
Platform Quick-Reference

The one rule that applies on every platform: always fetch the complete repository history before scanning. Most CI platforms default to shallow clones for speed — great for build times, terrible for security scanning. Secrets committed months ago become completely invisible to a shallow scan.
Part 6: When Something Gets Through — Your Response Playbook
Even well-defended systems have incidents. Speed matters more than anything else once an exposure is confirmed. Work through this sequence in order:
- Revoke the credential before anything else. Don’t wait until history is clean or the team is notified. Treat every exposed credential as already harvested — revoke it immediately, and this is true whether the repository is private or public.
- Rewrite the commit history. Use
git filter-repo(the current recommended approach) or BFG Repo Cleaner to surgically remove the secret from every commit where it appeared. - Force push and coordinate the team. Rewriting history requires a force push, and everyone working from the old history will need to re-clone or carefully rebase. Communicate the plan before executing.
- Scan the cleaned repository. Run
gitleaks git .after the rewrite to confirm nothing else is hiding in history. - Audit your pipeline artefacts. CI systems frequently capture build logs, environment dumps, and debug output. Verify the exposed credential didn’t appear in any stored artefact that’s still accessible.
⚠️ On GitHub Caching
GitHub maintains cached views of repository content that can persist even after a force push rewrites history. If your repository was public at any point during the exposure window, assume the credential has already been collected somewhere. Rotate first — history cleanup closes the door, it doesn’t undo what’s already been seen.
Pre-Launch Security Checklist
Before shipping any project, verify each of these is in place:
□ **.gitignore** covers environment files, private keys, credential stores, cloud configs, and logs
□ **.env.example** is committed with realistic-looking placeholder values for every required variable
□ **.pre-commit-config.yaml** is committed with Gitleaks pinned at v8.30.0
□ **pre-commit install** is documented in the project README and confirmed on every contributor's machine
□ **.gitleaks.toml** is configured to suppress false positives from documentation paths and includes any organisation-specific token patterns
□ Retrospective scan complete — gitleaks git . has been run against the full repository history at least once
□ CI/CD pipeline has Gitleaks running on all pull requests with full history checkout enabled
References
- Gitleaks Official Site
- GitHub
- [Gitleaks v8.30.0 Release Notes](http://github.com/gitleaks/gitleaks/releases · November 2025)
- pre-commit Framework Documentation
- GitHub Docs: Removing Sensitive Data from a Repository
- OWASP Secrets Management Cheat Sheet
- Git Reference: gitignore
- gitleaks/gitleaks-action
- BFG Repo Cleaner
- GitLab CI/CD Documentation
- Azure Pipelines Documentation
- Bitbucket Pipelines Documentation
- Jenkins Pipeline Syntax Reference
Further Reading — DevSecOps Series
What is DevSecOps & Why It Matters The foundations of DevSecOps — what it is, why it matters, and how security shifts left. 🔗 Read on Medium
Article 2 → Hardening Your Git Workflow: .gitignore, Gitleaks & Pipeline Secret Scanning Hands-on setup of pre-commit hooks, catching secrets before they hit GitHub, and building team habits from day one. 📍 You are here
Article 3 → Dependency Security — Automating SCA with Dependabot and Open Source Vulnerability Scanning
메타데이터
- post_id
- 0f7f6a753cb3
- slug
- hardening-your-git-workflow-gitignore-gitleaks-and-pipeline-secret-scanning-0f7f6a753cb3
- url
- https://medium.com/devsecops-ai/hardening-your-git-workflow-gitignore-gitleaks-and-pipeline-secret-scanning-0f7f6a753cb3
- canonical_url
- https://medium.com/devsecops-ai/hardening-your-git-workflow-gitignore-gitleaks-and-pipeline-secret-scanning-0f7f6a753cb3
- author_url
- https://medium.com/@gautammakwana421
- status
- ok
- fetched_at
- 2026-07-11 03:16:26