Pre-commit hooks are the fastest feedback loop in your security toolchain. A developer writes code, attempts to commit, and gets immediate feedback about security issues before the code ever reaches the repository. No waiting for CI. No context-switching back to fix something flagged 20 minutes later. Immediate, local, and fast.
The catch is that developers will disable hooks that are slow, noisy, or annoying. So the goal is building a hook suite that catches real issues quickly and quietly.
Why Pre-Commit Hooks Matter for Security
The earlier you catch a security issue, the cheaper it is to fix. A hardcoded AWS key caught at commit time takes 30 seconds to fix. The same key caught by a scanner in CI takes a few minutes. That key discovered in a public repository by an automated scraper takes hours of incident response, credential rotation, and audit.
Pre-commit hooks are particularly effective for:
- Secret detection - The highest-value pre-commit check. Catches credentials before they enter git history.
- Dockerfile linting - Catches insecure patterns like running as root or missing version pins.
- IaC scanning - Flags misconfigured Terraform, CloudFormation, or Kubernetes manifests.
- Dangerous function detection - Catches use of eval(), shell execution with user input, and similar patterns.
Setting Up the Framework
The pre-commit framework (pre-commit.com) is the standard tool. It manages hooks from multiple sources, handles installation, and updates cleanly.
Install it:
pip install pre-commit
Create .pre-commit-config.yaml in your repository root:
repos:
# Secret detection
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
# Private key detection
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: detect-private-key
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-merge-conflict
# Dockerfile linting
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint-docker
# Terraform security
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_tfsec
# YAML/JSON validation
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: check-json
- id: pretty-format-json
args: ['--autofix', '--no-sort-keys']
Install the hooks:
pre-commit install
Now every git commit runs these checks automatically.
Secret Detection in Detail
Secret detection is the single most impactful pre-commit hook. Here is how to configure detect-secrets properly.
Initial Baseline
Generate a baseline of existing secrets (you will need to address these separately):
detect-secrets scan > .secrets.baseline
Review the baseline and mark known false positives:
detect-secrets audit .secrets.baseline
The baseline file ensures that existing findings do not block every commit. Only new secrets trigger the hook.
Custom Patterns
Add patterns specific to your organization:
# .detect-secrets.yaml
plugins_used:
- name: ArtifactoryDetector
- name: AWSKeyDetector
- name: AzureStorageKeyDetector
- name: BasicAuthDetector
- name: CloudantDetector
- name: GitHubTokenDetector
- name: HexHighEntropyString
hex_limit: 3
- name: IbmCloudIamDetector
- name: JwtTokenDetector
- name: KeywordDetector
keyword_exclude: ''
- name: MailchimpDetector
- name: NpmDetector
- name: PrivateKeyDetector
- name: SendGridDetector
- name: SlackDetector
- name: SoftlayerDetector
- name: StripeDetector
- name: TwilioKeyDetector
Handling False Positives
When a detection is a false positive, add an inline comment:
SECRET_PATTERN = "sk_test_.*" # pragma: allowlist secret
Or add the file path to the baseline exclusion:
detect-secrets scan --exclude-files 'tests/fixtures/.*' > .secrets.baseline
Dockerfile Security Hooks
Hadolint catches common Dockerfile anti-patterns that have security implications:
# Hadolint will flag these:
FROM ubuntu # DL3006: Always tag the version
RUN apt-get update # DL3009: Delete apt-get lists after install
USER root # DL3002: Last user should not be root
Configure a .hadolint.yaml for your standards:
ignored:
- DL3008 # Pin versions in apt get install (can be noisy)
trustedRegistries:
- docker.io
- gcr.io
- your-registry.com
Infrastructure as Code Scanning
For Terraform, tfsec catches security misconfigurations before they are applied:
# tfsec will flag this:
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
# Missing: encryption, logging, public access block
}
For Kubernetes manifests, use kubesec:
- repo: https://github.com/controlplaneio/kubesec
rev: v2.13.0
hooks:
- id: kubesec
Performance Optimization
The biggest risk to hook adoption is speed. Here is how to keep things fast.
Only scan changed files. Most hooks automatically only check staged files. Verify this is the case for each hook.
Skip hooks on non-relevant files. Use the types and files filters:
- id: hadolint-docker
files: Dockerfile
types: [file]
- id: terraform_tfsec
files: \.tf$
types: [file]
Use local caching. Pre-commit caches hook environments. The first run is slow; subsequent runs are fast.
Set a time budget. Total pre-commit execution should stay under 10 seconds. If it exceeds that consistently, developers will start using --no-verify.
Measure your hook performance:
time pre-commit run --all-files
If a specific hook is too slow, move it to CI instead.
Team Adoption Strategy
Make installation automatic. Add a setup script to your repository:
#!/bin/bash
# setup.sh
pip install pre-commit
pre-commit install
echo "Pre-commit hooks installed successfully."
Reference it in your contributing guide and onboarding documentation.
Do not block on informational findings. Configure hooks with appropriate exit codes. Hooks that block commits should have near-zero false positives. Hooks that are informational should warn but not prevent the commit.
Provide escape hatches. Sometimes developers need to commit despite a finding (a test fixture that looks like a secret, for example). Document the --no-verify flag but also provide the proper way to handle false positives (allowlists, baseline updates).
Track adoption. You cannot force pre-commit hook installation on developer machines, but you can verify in CI:
- name: Verify pre-commit hooks would pass
run: pre-commit run --all-files
This catches issues from developers who skipped hooks locally.
Maintenance
Update hook versions regularly. Run monthly:
pre-commit autoupdate
Review the secrets baseline quarterly. Old secrets in the baseline should be rotated and removed.
Collect feedback from developers. If a hook consistently produces false positives, fix the configuration or remove the hook. A hook that developers disable is worse than no hook at all because it creates a false sense of security.
How Safeguard.sh Helps
Safeguard.sh complements pre-commit hooks by providing the server-side verification layer. While hooks catch issues locally before commits, Safeguard.sh validates the same checks in your CI/CD pipeline to ensure nothing slips through when hooks are bypassed. It provides the organizational visibility to know which projects have dependency vulnerabilities, regardless of whether individual developers have their local hooks configured. Together, pre-commit hooks and Safeguard.sh create a defense-in-depth approach where local checks catch issues early and platform checks ensure nothing reaches production unchecked.