Writing pre-commit Hooks for Ops Repos with AI (Catch It Before It Lands)
pre-commit hooks stop bad commits at the source. Use AI to draft custom Bash and Python hooks, then review them so they fail loud and never leak secrets.
- #bash
- #python
- #pre-commit
- #git
- #automation
The most expensive bug I ever shipped was a Terraform variable with a trailing space that the apply silently accepted and then drifted half our staging environment. A four-line check would have caught it before it ever reached a PR. That’s the entire pitch for pre-commit hooks: catch the cheap, dumb, recurring mistakes at the moment of commit, when the fix costs nothing, instead of in CI ten minutes later or in production three days later. Writing the hooks themselves is repetitive plumbing — exactly the kind of bounded scripting where an AI assistant moves fast and you stay the reviewer.
I treat the assistant as a quick junior engineer. It drafts the hook; I decide whether it blocks my commits.
The framework, not raw .git/hooks
You can drop a script in .git/hooks/pre-commit, but it lives outside version control and nobody on your team gets it. The pre-commit framework fixes that: hooks are declared in a .pre-commit-config.yaml that’s committed, so the whole team shares them.
# .pre-commit-config.yaml
repos:
- 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-merge-conflict
Install once with pre-commit install, and now every git commit runs these. The off-the-shelf hooks cover the basics. The interesting work — and where AI helps most — is the custom checks specific to your repo.
Drafting a custom local hook
A local repo block lets you run your own scripts as hooks. Here’s the pattern, and it’s a great thing to ask an AI to scaffold:
- repo: local
hooks:
- id: no-debug-prints
name: Block stray debug prints
entry: scripts/no-debug-prints.sh
language: script
types: [python]
And the script the AI drafts for you:
#!/usr/bin/env bash
set -euo pipefail
# Args are the staged files pre-commit passes in
failed=0
for file in "$@"; do
if grep -nE 'print\(|breakpoint\(\)|pdb\.set_trace' "$file"; then
echo "ERROR: debug statement in $file" >&2
failed=1
fi
done
exit "$failed"
The thing to verify here is the exit code. A hook that doesn’t exit 1 on failure is decorative — it prints a warning and lets the commit through anyway. The most common AI mistake in hooks is logging the problem but returning 0. Read the last line of every generated hook.
Pro Tip: pre-commit passes the staged filenames as arguments, but they’re the working-tree versions — if you git add then edit again, the hook may see content that won’t be committed. For checks that must inspect exactly what’s staged, have the hook read from git show :"$file" rather than the file on disk.
The secret-scanning hook is the one to get right
Every ops repo needs a hook that blocks committed credentials, because the one time it matters, it really matters. You can use a maintained tool — and you should — but understanding the shape helps you review it.
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
When you ask an AI to write a custom secret check, scrutinize it harder than anything else, because a leaky regex gives false confidence. A naive version misses base64-encoded keys, multi-line PEM blocks, and anything that doesn’t match its narrow pattern. Prefer the battle-tested tool over a hand-rolled regex, and if you do roll your own, treat it as a backstop, never the primary defense. And the obvious meta-point: don’t paste real secrets into your AI prompt to “test the detector.” Use fake values shaped like the real thing.
A Python hook for structured validation
When the check needs real parsing — validating that every Kubernetes manifest has resource limits, say — a Python hook beats grep:
#!/usr/bin/env python3
"""Fail if any staged k8s Deployment lacks resource limits."""
import sys
import yaml
def has_limits(doc: dict) -> bool:
containers = (
doc.get("spec", {})
.get("template", {})
.get("spec", {})
.get("containers", [])
)
return all(c.get("resources", {}).get("limits") for c in containers)
def main(paths: list[str]) -> int:
rc = 0
for path in paths:
with open(path) as fh:
for doc in yaml.safe_load_all(fh):
if not doc or doc.get("kind") != "Deployment":
continue
if not has_limits(doc):
print(f"ERROR: {path} Deployment missing resource limits", file=sys.stderr)
rc = 1
return rc
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
yaml.safe_load_all handles multi-document files — the thing a grep-based hook can’t do reliably. When the AI drafts this, check two things: it uses safe_load (never plain load, which can execute arbitrary tags), and it returns a non-zero exit code. Both are easy to miss in a quick read.
Make the failure message actually helpful
A hook that fails with no explanation just trains people to run git commit --no-verify. When reviewing AI output, push for failure messages that say what’s wrong and how to fix it:
echo "ERROR: $file has trailing whitespace on line $line" >&2
echo "Fix: run 'pre-commit run trailing-whitespace --all-files'" >&2
The difference between a hook teams keep and a hook teams disable is almost entirely the quality of its error message. This is worth a specific prompt: “rewrite these hook failures to name the file, the line, and the exact command to fix it.”
Keep hooks fast or they get bypassed
A pre-commit hook that takes eight seconds will be skipped within a week. Ask the AI to make hooks operate only on staged files (which pre-commit already scopes for you) and to short-circuit early. If a check is genuinely slow — a full security scan — move it to CI or a pre-push hook and keep pre-commit under a second or two. Speed is a feature here; a hook nobody runs catches nothing.
Where this fits the bigger picture
Pre-commit hooks are the cheapest layer of a defense-in-depth setup that runs up through CI and a real code review before anything merges. The hooks catch mechanical mistakes; humans catch the judgment calls. I draft mine with Claude or Cursor and keep the working ones in a prompt workspace so the next repo starts with a vetted set. The reusable prompts live in our prompt library, with deeper bundles in the prompt packs, and the rest of the scripting patterns are in the Bash and Python automation category.
The rule that holds
A pre-commit hook runs on every engineer’s machine and decides what’s allowed into history, which makes a buggy one quietly corrosive — it either blocks good commits or waves through bad ones. Let the AI draft the plumbing fast, but you verify the exit codes, the safe_load, the failure messages, and above all that no secret-scanning hook was tested with a real secret. Quick junior, human reviewer, no live credentials in the prompt. That’s the whole discipline.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.