Git Hooks: Preventing Your Credentials from Going Viral

The solution architect leaned back in her chair, coffee in hand, reviewing the morning’s pull requests when Slack exploded. The security team had detected AWS credentials in the company’s public GitHub repository: credentials with full admin access to production. The commit had been live for exactly 47 minutes, just long enough for automated scanners to find them and spin up $12,000 worth of cryptocurrency mining instances. The developer who committed them? A senior engineer with eight years of experience, who’d been pair programming with an AI assistant and simply didn’t notice when it helpfully included the credentials from a local config file.
We’ve all heard these horror stories. Maybe you’ve even lived through one. The post-mortem always reads the same: "Developer error. Will implement additional training." But we’re human. We make mistakes. And in the age of AI-assisted coding, where we’re generating and moving code faster than ever, our traditional safety nets are showing their age.
Our Safety Nets Have Holes
Git hooks have been part of the Git ecosystem since 2005, nearly two decades. Yet walk into most development teams, and you’ll find them either completely unused or implemented as an afterthought that developers routinely bypass with --no-verify. It’s the security equivalent of having a smoke detector with the batteries removed.
The traditional argument went something like this: "We have CI/CD pipelines that catch everything. Pre-commit hooks just slow developers down." And for a while, that made sense. Why run checks locally when your build server will catch issues in a few minutes anyway?
But the landscape has shifted. AI coding assistants like GitHub Copilot, Cursor, and Claude Code are phenomenal productivity boosters, but they’re also creating new risks:
- They pull code from context windows that might include secrets
- They generate boilerplate that might not match your security standards
- They move so fast that our usual "look before you leap" instincts don’t engage
- They make it easy to commit large volumes of code without careful review
The data backs this up: GitGuardian’s research found that repositories with Copilot enabled have a 40% higher incidence rate of leaked secrets compared to the baseline of all public repositories (6.4% vs 4.6%).
According to GitGuardian’s State of Secrets Sprawl 2025 report, there were 23.77 million new secrets detected in public GitHub commits in 2024 alone, a 25% surge from the previous year. Even more alarming: 70% of valid secrets detected in public repositories in 2022 remain active today, and 35% of all private repositories contain hardcoded secrets. The problem isn’t getting better, it’s accelerating.
The "Too Little, Too Late" Principle
Think of your development workflow like airport security. You could theoretically check everyone’s bags only when they arrive at their destination. It would work. Eventually you’d catch the contraband. But by then, the person has already boarded the plane, flown across the country, and potentially caused damage.
Pre-commit hooks are like the security checkpoint before you board. They’re not perfect, and they shouldn’t be your only security measure, but they catch problems at the moment when they’re easiest and cheapest to fix before they leave your machine.
The beauty of pre-commit hooks lies in their position in the workflow. They operate at the last possible moment before code becomes part of your repository’s history. Once something is committed, even if you later remove it, it remains in your Git history. Tools exist to scrub history, but they’re painful, disruptive, and often incomplete.
Not all checks belong in the same place. Your comprehensive test suite, linting rules, and build verification? Those belong in CI. But certain checks, especially those preventing irreversible mistakes, belong at the commit stage.
What Makes a Good Pre-Commit Check?
Before we dive into implementation, let’s establish some ground rules. A good pre-commit check should be:
Fast - If your hook takes more than a few seconds, developers will bypass it. Keep it under 5 seconds for most commits, 10 seconds maximum for large changesets.
Focused - Don’t try to run your entire test suite. Target specific, high-value checks that prevent immediate disasters.
Deterministic - The same code should always produce the same result. Flaky checks train developers to ignore failures.
Informative - When it fails, it should clearly explain what’s wrong and how to fix it.
Here’s a hierarchy of checks that work well in pre-commit hooks:
| Priority | Check Type | Why It Belongs Here | Typical Runtime |
|---|---|---|---|
| Critical | Secret scanning | Prevents irreversible leaks | 1-3 seconds |
| High | Syntax validation | Catches broken code before commit | 0.5-2 seconds |
| High | Merge conflict markers | Prevents obvious mistakes | 0.1 seconds |
| Medium | File size limits | Prevents accidental large file commits | 0.1 seconds |
| Medium | Trailing whitespace | Reduces noise in diffs | 0.2 seconds |
| Low | Code formatting | Better in CI or editor | 1-5 seconds |
| Low | Full linting | Better in CI | Variable |
Notice what’s not on this list: running your test suite, building Docker images, or running comprehensive static analysis. Those belong in CI where they can run in parallel and won’t block developers.
Scanning for Secrets
Let’s get specific about the most critical pre-commit check: secret scanning. This is where pre-commit hooks bring the most value because the cost of failure is so high.
Modern secret scanners work by looking for patterns that match common secret formats:
- Entropy analysis: Random-looking strings with high entropy (like API keys)
- Regex patterns: Known formats for AWS keys, GitHub tokens, private keys, etc.
- Keyword detection: Variables named "password", "secret", "api_key", etc.
Understanding what you’re protecting against matters: 58% of all detected secrets in 2024 were generic secrets, hardcoded passwords, database connection strings, and custom authentication tokens that lack standardized patterns. These are the fastest-growing category because they often bypass platform-level protections like GitHub’s Push Protection.
The gold standard tool for this is detect-secrets from Yelp, though alternatives like gitleaks and truffleHog are also excellent. Here’s why detect-secrets works particularly well:
- It creates a baseline of known secrets (like test fixtures)
- It scans only new changes, keeping it fast
- It has low false-positive rates
- It’s actively maintained and understands modern secret formats
The Anatomy of a Secret Leak
Before we implement our hook, let’s understand what we’re protecting against. Secrets typically leak in three ways: Direct inclusion - The most obvious:
# Don’t do this!
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
DATABASE_URL = "postgresql://admin:P@ssw0rd123@prod-db.company.com:5432/maindb"
Environment file commits - Accidentally committing .env:
# .env (should NEVER be committed)
STRIPE_SECRET_KEY=sk_live_51HqR8uL...
OPENAI_API_KEY=sk-proj-abc123...
AI-assisted inclusion - The sneaky one:
# Developer asks AI: "Write a function to upload to S3"
# AI helpfully includes example credentials from context
def upload_file(filename):
s3_client = boto3.client(
's3',
aws_access_key_id='AKIAIOSFODNN7EXAMPLE', # AI grabbed this from somewhere
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
)
Time to Build Our Safety Net
Let’s implement a practical pre-commit hook setup that’s fast, effective, and won’t make your developers want to throw their laptops out the window.
Step 1: Choose Your Framework
You could write hooks as raw bash scripts in .git/hooks/pre-commit, but that’s the hard way. Instead, use the pre-commit framework, which manages hooks across your team and makes them easy to configure.
Install it:
# Using pip
pip install pre-commit detect-secrets
# Using homebrew (macOS)
brew install pre-commit detect-secrets
# Using conda
conda install -c conda-forge bc-detect-secrets
Step 2: Create Your Configuration
In your repository root, create .pre-commit-config.yaml:
# .pre-commit-config.yaml
repos:
# Secret scanning - THE critical check
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
exclude: package-lock.json
# Fast syntax checks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
# Catch merge conflicts
- id: check-merge-conflict
# Prevent large files (>500KB)
- id: check-added-large-files
args: ['--maxkb=500']
# Ensure files end with newline
- id: end-of-file-fixer
# Catch common mistakes
- id: check-yaml
- id: check-json
- id: check-toml
# Prevent committing to main directly
- id: no-commit-to-branch
args: ['--branch', 'main', '--branch', 'master']
# Language-specific fast checks
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
# Only check syntax, not style
args: ['--select=E9,F63,F7,F82', '--show-source']
This configuration is deliberately minimal. Notice what we’re NOT doing:
- No code formatting (use editor plugins instead)
- No comprehensive linting (save for CI)
- No test execution (definitely CI territory)
Step 3: Initialize Secret Scanning
Create your secrets baseline:
# Generate initial baseline
detect-secrets scan > .secrets.baseline
# Review and audit the baseline
detect-secrets audit .secrets.baseline
The baseline file marks known "secrets" that are actually safe (like test fixtures or example keys). This dramatically reduces false positives.
Step 4: Install the Hooks
# Install hooks for your repository
pre-commit install
# Test against all files (do this once)
pre-commit run --all-files
This creates the actual Git hook in .git/hooks/pre-commit that will run automatically.
Step 5: Handle the Inevitable False Positives
Sometimes legitimate code triggers secret detection. You have two options: Option 1: Add to baseline (for permanent test fixtures)
# Add specific false positives to baseline
detect-secrets scan --baseline .secrets.baseline
Option 2: Inline pragma (for one-off cases)
# This is a public test key from AWS documentation
TEST_KEY = "AKIAIOSFODNN7EXAMPLE" # pragma: allowlist secret
Team-Wide Enforcement
Here’s the challenge: pre-commit hooks live in .git/hooks, which isn’t tracked by Git. How do you ensure everyone on your team uses them?
Strategy 1: Document in README
## Development Setup
After cloning, run:
```bash
pip install pre-commit
pre-commit install
```
Strategy 2: Add to CI
# .github/workflows/ci.yml
- name: Verify pre-commit hooks would pass
run: |
pip install pre-commit
pre-commit run --all-files
This catches commits from developers who bypassed hooks.
Strategy 3: Setup script
#!/bin/bash
# scripts/setup-dev-environment.sh
echo "Setting up development environment..."
# Install pre-commit
if ! command -v pre-commit &> /dev/null; then
echo "Installing pre-commit..."
pip install pre-commit
fi
# Install hooks
pre-commit install
echo "✓ Pre-commit hooks installed"
The Escape Hatch
Let’s be realistic: sometimes you need to bypass the hooks. Maybe you’re committing a known issue to fix later, or you’re in the middle of a critical hotfix.
# Bypass hooks for a single commit
git commit --no-verify -m "Emergency hotfix - will clean up"
But here’s the rule: If you find yourself using --no-verify regularly, your hooks are too strict. They should catch real problems, not annoy developers with false positives or slowness.
Red flags that your hooks need tuning:
- Taking more than 10 seconds on typical commits
- False positive rate above 5%
- Developers complaining more than once a week
- More than 10% of commits use
--no-verify
The Reality Check
Let’s test our setup with real scenarios:
Test 1: Catch a secret
# Create a file with a fake AWS key
echo 'AWS_KEY="AKIAIOSFODNN7EXAMPLE"' > config.py
git add config.py
git commit -m "Add config"
# Should fail with:
# Detect secrets...................................................Failed
# - hook id: detect-secrets
# - exit code: 1
Test 2: Verify performance
# Time a normal commit
time git commit -m "Test commit"
# Should complete in under 5 seconds
Test 3: Check CI integration
# Simulate what CI will do
pre-commit run --all-files
# Should match local results
Your Pre-Commit Checklist
Implementing pre-commit hooks effectively comes down to these principles:
- Keep it fast: Under 5 seconds for normal commits, or developers will bypass it
- Focus on prevention: Catch irreversible mistakes like secret leaks, not style issues
- Make it easy: Use frameworks like
pre-commitrather than raw bash scripts - Balance security and usability: Too strict and people bypass it; too loose and it’s worthless
- Enforce in CI too: Catch commits from developers who bypassed hooks
- Monitor and tune: Track false positives and adjust patterns as needed
The question is whether you can afford not to. That $12,000 cryptocurrency mining bill from the opening story? The actual cost was closer to $45,000 when you factor in incident response, credential rotation, security audits, and the engineer’s time. According to IBM’s Cost of a Data Breach report, breaches involving stolen or compromised credentials take an average of 292 days to identify and remediate, more than any other attack vector. The pre-commit hook that would have prevented it? About 30 minutes to set up.
Here’s what I want you to think about: How many commits happened in your organization today? How many of those were reviewed by a human before merging? How many were generated with AI assistance? And most importantly, how confident are you that none of them contained secrets? Consider this: 15% of commit authors leaked a secret in 2024, and 4.6% of all public repositories contain at least one secret.
If that question makes you uncomfortable, you know what to do. Start with secret scanning. Keep it fast. Make it easy. Your future self (and your security team) will thank you.
Share this article
Enjoyed this article?
Subscribe to get more insights delivered to your inbox monthly
Subscribe to Newsletter