INTRODUCTION
In today’s modern software development and devOps, security should not be an afterthought especially with AI helping to write most of the code. When I joined my current company three years ago, our release was bi-annually mainly because the team: developers, security team, DevOps spend most of their time fighting and fixing trvial issues that should not find their way to CI. A linting error that takes three milliseconds to catch on a developer laptop was costing us about 20 minutes of pipeline. A developer or DevOps engineer would open a PR, the CI queues for 25 minutes and then fail on something fixable in the developer’s local development environment, like a missing comma, trailing whitespace violation, worst of all, an AWS secret key committed directly into source code. The question I asked myself is why are we discovering these problems in CI? CI is expensive and these problems should not make their way to CI. What if we caught all of this before code is pushed? Well, the answer, it turned out is git hooks (client-side hooks).
WHAT’S A GIT HOOK REALLY?
A hook is simply a script that Git executes automatically when a specific event occurs. For example, the pre-commit hook fires before Git records a commit, giving you a chance to inspect the staged changes and abort if something looks wrong. Using client-side hooks pre-commit hooks ensures that your security posture is shifted left. Shift-left security is a security mindset where all necessary security measures are put in place from the onset of code development. The further left you shift security in your development lifecycle you catch a problem, the cheaper it is to fix. A secret caught on a developer’s machine costs thirty seconds. The same secret caught in a code review costs thirty minutes. Caught in production? You are looking at an incident, a key rotation, a postmortem, and a very uncomfortable call with your security team. Many teams avoid pre-commit hooks due to perceived developer friction, inconsistent enforcement, and the lack of standardized tooling. However, the pre-commit framework solves these challenges by providing version-controlled, shareable, and consistent hook management across all developer environments while preventing costly CI/CD pipeline failures.
In this tutorial we will learn how we use the pre-commit framework to increase our security posture with shift-left security after months of iteration.
INSTALLATION
First, install the framework and wire it into a repository.
If you don’t have Python installed, grab it from the official documentation, then create a virtual environment and install the framework with pip:
pip install pre-commit
Confirm it installed correctly:
pre-commit --version
Configure the Framework
The .pre-commit-config.yaml file tells the framework which hooks to run before code is committed. It lives at the root of your repository and defines which repos and hooks are wired in.
Basic structure:
repos:
- repo: <repository-url>
rev: <version/tag>
hooks:
- id: <hook-id>
Create the config file at the root of your repo:
touch .pre-commit-config.yaml
Organizing Hooks Into Layers
Rather than dumping every hook into one flat list, we organized ours into clear layers, each with a distinct job:
repos:
# ── Layer 1: Code Quality & SAST ──────────────────────────────
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.1
hooks:
# -- Code quality --
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- id: terraform_docs
# -- SAST / IaC scanning --
- id: terraform_trivy
- id: terraform_checkov
- id: terrascan
# ── Layer 2: Secret Detection ─────────────────────────────────
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/gitguardian/ggshield
rev: v1.51.0
hooks:
- id: ggshield
language_version: python3
stages: [pre-commit]
# ── Layer 3: File Hygiene ──────────────────────────────────────
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: ['--maxkb=500']
- id: no-commit-to-branch
args: ['--branch', 'main', '--branch', 'production']
- id: check-merge-conflict
- id: check-case-conflict
Layer 1 — Code Quality & SAST handles everything Terraform-related: formatting, validation, linting, docs generation, and static analysis via Trivy, Checkov, and terrascan. We dropped terraform_tfsec from this layer once tfsec reached end-of-life and was merged into Trivy — running both was just producing duplicate findings.
Layer 2 — Secret Detection is arguably the highest-leverage layer in the whole setup. Gitleaks and detect-secrets both scan staged changes for credentials, API keys, and tokens before they ever touch the remote. ggshield adds GitGuardian’s detection on top — just be aware it requires a GITGUARDIAN_API_KEY set in every developer environment and every CI runner. Skip that step and every commit gets blocked.
Layer 3 — File Hygiene catches the small, dumb stuff: trailing whitespace, missing end-of-file newlines, malformed YAML/JSON, accidentally-committed large files, unresolved merge conflict markers, and direct commits to protected branches like main or production.
Wiring It Up
Install the git hook scripts. At this stage we will install all the hooks by running this command “ pre-commit install” to set up the git hook scripts
pre-commit install
After this , pre-commit hooks will run automatically on git commit!
Testing the hooks with various scenarios
Scenario A — Clean commit. The happy path. A well-written commit clears all three layers in under eight seconds.
Scenario B — Hardcoded AWS secret. A developer accidentally stages a file containing a real AWS secret access key. Without pre-commit hooks, this sails straight through to the remote repository. With them, the commit is blocked on the spot, before the secret ever leaves the developer’s machine.
Scenario C — Lint failure, auto-fixed and blocked. Ruff is configured with --fix, so safe issues get corrected automatically. The commit still gets blocked so the developer can review exactly what changed — but there's no manual editing required. Just re-stage and commit.
THE SECURITY POSTURE IMPACT WIN
We ran a retrospective three months after full rollout. The results were better than any of us expected.
Secrets-related incidents per quarter: 4–6 → 0–1 (↓ 91%)
Hardcoded credentials in git, per month: 8–12 → 0
Mean time to detect a secret leak: days (post-deploy) → seconds (at commit)
CI failures attributable to preventable noise: 68% → collapsed almost entirely
Overall CI pipeline failure rate: ↓ 73% within sixty days
Before pre-commit hooks, we were spending the majority of our pipeline budget on problems that never should have reached CI in the first place. After rollout, the pipelines that remained were surfacing real problems — which is exactly what CI is supposed to do.
The CI mirror: closing the — no-verify escape hatch
Here’s the catch: pre-commit hooks are only as strong as their enforcement. Any developer can bypass them entirely with:
git commit --no-verify
Local hooks are a convenience layer, not a security boundary. To actually close that gap, we mirror the exact same hook configuration in GitHub Actions on every pull request. If something slips past a developer’s local environment — intentionally or not — it gets caught at the PR stage instead.
That combination — fast local feedback plus a CI mirror that can’t be skipped — is what actually shifted our security posture left for good.
Takeaways
If you’re on the fence about pre-commit hooks because of the perceived setup cost, here’s the honest tradeoff: a few hours configuring .pre-commit-config.yaml and onboarding your team versus months of cumulative CI time wasted on problems that should never have left a developer's laptop.
Start small. A secret-detection layer alone (gitleaks or detect-secrets) will likely pay for itself in the first incident it prevents. Layer in file hygiene next, then code-quality and IaC scanning as your stack matures. And don’t forget the CI mirror — without it, your shift-left strategy has a hole in it that any developer can walk through with one flag.



Top comments (0)