There is a cost curve to fixing a security issue, and it climbs steeply the later you catch it. A secret caught before the commit exists costs a developer a few seconds and zero exposure. The same secret caught in production costs a credential rotation, an incident review, and possibly a breach disclosure. Pre-commit security hooks sit at the very bottom of that curve — the earliest technical checkpoint that exists — and they give developers feedback in their own terminal, in the flow of work, before anything leaves their machine.
They are not your enforcement boundary (more on that limitation below), but as a fast, local first line, nothing beats them.
What a pre-commit hook is
A Git pre-commit hook is a script that Git runs automatically before a commit is finalized. If the script exits non-zero, the commit is aborted. Security hooks use this moment to scan the staged changes for problems — hardcoded secrets, obvious code vulnerabilities, insecure Infrastructure-as-Code, large binary blobs — and stop the commit if they find one. The developer sees the issue instantly, fixes it, and re-commits, all without a round trip to CI.
The de facto standard for managing these is the pre-commit framework (a Python tool, but language-agnostic in what it runs). It manages hook installation, versioning, and execution from a single .pre-commit-config.yaml file checked into the repo, so every developer on the team gets the same hooks.
A working security-focused config
Here is a practical starting configuration that layers several security checks:
# .pre-commit-config.yaml
repos:
# 1. Secrets detection
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
# 2. Infrastructure-as-Code security (Terraform, etc.)
- repo: https://github.com/bridgecrewio/checkov
rev: 3.2.0
hooks:
- id: checkov
args: ["--quiet", "--compact"]
# 3. Hygiene checks that prevent accidental leaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: detect-private-key # blocks committed private keys
- id: check-added-large-files # catches accidental data/binary dumps
- id: check-merge-conflict # blocks unresolved conflict markers
Install it once per clone:
pip install pre-commit
pre-commit install # wires the git hook
pre-commit run --all-files # optional: scan the whole repo now
From this point, gitleaks, checkov, and the hygiene checks run automatically on staged files at every git commit, and a hit aborts the commit with a clear message.
Pick hooks by language and stack
The config above is a base; layer stack-specific security linters on top:
| Stack | Useful pre-commit security hook |
|---|---|
| Python | bandit (common insecure patterns) |
| JavaScript / TS | eslint with a security plugin |
| Terraform / IaC | checkov, tfsec |
| Docker | hadolint (Dockerfile best practices) |
| Any | gitleaks / trufflehog (secrets), detect-private-key |
Resist the urge to enable everything. Each hook adds latency and potential false positives, and a slow, noisy pre-commit stage is one that developers disable. Start with secrets detection (the highest-value, lowest-noise check) and add one or two stack-specific hooks that your team will actually respect.
The three rules that keep hooks from being disabled
Pre-commit hooks live or die by developer patience. Three rules protect it:
- Keep them fast. Hooks should run on staged files only, not the whole repo, so latency scales with the size of the change, not the size of the codebase. A pre-commit stage that adds more than a couple of seconds to a normal commit will get bypassed.
- Keep them low-noise. A hook that cries wolf gets ignored, then disabled. Tune allowlists for known test fixtures and example values before rolling a hook out team-wide.
- Make the config shared and pinned. Check
.pre-commit-config.yamlinto the repo with pinnedrev:versions so everyone runs identical hooks and an upstream change cannot silently alter behavior mid-sprint.
The critical limitation: hooks are bypassable
This is the point that determines your whole architecture. Pre-commit hooks run on the developer's machine, are opt-in (someone has to run pre-commit install), and are trivially bypassed with git commit --no-verify. That is fine — they are a convenience and a first line, not a control. Never treat pre-commit hooks as your security enforcement boundary. Every check you run in pre-commit must also run server-side in CI, where it cannot be skipped, so a developer who bypasses the hook (deliberately or on a fresh clone that never installed it) still gets caught before merge.
The correct mental model is a funnel: pre-commit gives fast local feedback and catches most issues cheaply; CI is the actual gate that guarantees nothing risky merges. Same checks, two layers, different jobs.
# ci: the enforcement layer that mirrors the local hooks
security-checks:
stage: security
script:
# same secrets + IaC checks, but here they cannot be --no-verify'd
- safeguard scan --secrets --iac --sast --diff origin/main --fail-on high
A rollout that sticks
- Start with one repo and one hook. Prove secrets detection works and is low-noise before expanding.
- Ship the config, then socialize
pre-commit install. Add it to onboarding docs and repo READMEs; consider a bootstrap script that installs it automatically. - Add the CI mirror on day one. The hook is optional; the CI check is not. This is what makes it safe to keep the hooks lightweight.
- Expand by team consent. Let each team add the stack-specific hooks they value, rather than mandating a heavy config from the center.
How Safeguard helps
Safeguard is designed to run the same engine at both layers of the funnel. The Safeguard CLI plugs into pre-commit for instant local feedback on secrets, SCA, and code findings, then runs the identical checks in CI as the enforcement gate — so what a developer sees in their terminal is exactly what the pipeline enforces, with no drift between "local advice" and "the real rule." Griffin AI keeps the local pass low-noise by validating secrets and applying reachability, which is what keeps hooks fast and trusted enough that developers leave them installed. When a finding is fixable, auto-fix can propose the correction rather than just blocking the commit.
See how a single-engine, IDE-to-CI flow compares to stitching separate tools together in our platform comparison, then get started free or read the documentation.