AI-Assisted Pre-Commit Hooks for Automation Repos
Use AI like a fast junior engineer to build and refine pre-commit hooks that catch automation script bugs, leaked secrets, and bad config before they ever land.
- #automation
- #pre-commit
- #git
- #ci
I lost an afternoon once to a single unquoted variable in a Bash deploy script. It expanded to an empty string, the rm -rf "$TARGET"/ walked up a directory it should never have touched, and the only thing that saved us was that the runner happened to lack permissions. That bug was reviewable. A human could have caught it. The problem was that nobody looked at the right three lines before it merged. Pre-commit hooks exist precisely to look at those three lines every single time, automatically, so a tired human at 6pm does not have to.
These days I let an AI assistant do most of the grunt work of writing and refining those hooks. It is genuinely good at it. But I want to be clear about the relationship up front: the model is a fast junior engineer. It drafts hooks quickly, it knows the flags I forget, and it never gets bored writing the tenth yamllint config. It also confidently suggests hooks that would git push --force to main if you let it. So every hook it proposes gets read, scoped, and gated by me before it runs against anything that matters. The model writes; I decide.
Start with the framework, not bespoke scripts
The pre-commit framework is the right foundation for an automation repo because it manages hook environments for you and runs the same checks locally and in CI. Here is a real .pre-commit-config.yaml from a repo full of Bash and Ansible:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-added-large-files
args: ["--maxkb=512"]
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
hooks:
- id: shellcheck
args: ["--severity=warning"]
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
args: ["-c", ".yamllint.yaml"]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
Install it with pre-commit install, and run it across the whole repo once with pre-commit run --all-files to see the backlog. That first run is ugly. It is supposed to be. This is your blast radius made visible before any of it ships.
When I ask an AI to help here, I do not say “write me a pre-commit config.” I give it the actual file tree, the languages in the repo, and the hook versions I already trust, then ask it to fill gaps. A prompt that works: “This repo is Bash deploy scripts plus Ansible playbooks. Here is my current .pre-commit-config.yaml. Suggest additional hooks for shell and YAML safety, pin every rev to a real released tag, and explain what each one would have caught. Do not add anything that auto-fixes files I have not opted into.” That last sentence matters. Left unprompted, models love to add aggressive auto-formatters that rewrite your whole tree on first commit.
Make shellcheck non-negotiable
For automation repos, shellcheck is the single highest-value hook. My unquoted-variable disaster is SC2086, and shellcheck flags it instantly. The hook above runs at --severity=warning, which catches the dangerous classes without drowning you in style nags.
The AI’s role here is teaching, not just catching. When shellcheck fails, I paste the error and the offending function and ask the model to explain the failure mode and propose a fix. It will tell you that rm -rf $TARGET/ should be rm -rf "${TARGET:?TARGET is unset}/" and, more usefully, why the :? guard turns a silent empty expansion into a hard failure. That is the difference between a fix and an education. I cover the broader habit in writing idempotent automation scripts, because a hook that catches the bug is only half the discipline.
Pro Tip: add # shellcheck disable=SCxxxx inline only with a comment explaining why, and add a hook that rejects bare disables. A disable without a reason is a TODO someone will never come back to.
Catch secrets before they leave your machine
A leaked credential in git history is permanent and expensive. Two tools cover this: gitleaks and detect-secrets. I run gitleaks in the config above and keep detect-secrets as a baselined second layer:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]
exclude: "package-lock.json"
Generate the baseline once with detect-secrets scan > .secrets.baseline, audit it with detect-secrets audit .secrets.baseline, and commit it. The baseline is how you tell the tool “these specific high-entropy strings are known and fine” without disabling the whole check.
This is also where the no-prod-creds rule becomes concrete. The hook scans for credentials; it does not need any. Some teams get tempted to wire a hook that calls out to a secrets manager to “verify” a key is rotated. Do not give a commit-time hook live credentials to do that. A hook runs on every developer’s laptop, in whatever shell state they happen to have. If you want that verification, push it into a gated CI job with a scoped, short-lived token, not a hook with your prod vault address baked in.
Write a custom hook, and have the AI harden it
Framework hooks cover the common cases. Automation repos always have a house rule that needs a custom check. Mine: every shell script must set strict mode. Here is the hook, in scripts/check-strict-mode.sh:
#!/usr/bin/env bash
set -euo pipefail
fail=0
for f in "$@"; do
# Only check files that look like bash scripts
head -n1 "$f" | grep -qE '^#!.*\b(bash|sh)\b' || continue
if ! grep -qE '^set -[a-z]*e[a-z]* ' "$f" && \
! grep -qE '^set -o errexit' "$f"; then
echo "::error file=$f:: missing 'set -euo pipefail' (or equivalent)"
fail=1
fi
done
exit "$fail"
Wired into the config as a local hook:
- repo: local
hooks:
- id: strict-mode
name: require strict mode in shell scripts
entry: scripts/check-strict-mode.sh
language: script
types: [shell]
I wrote the first draft of that grep myself and it was wrong. It matched set -e inside a heredoc and missed set -o errexit. I handed it to an AI with the instruction: “Here is a pre-commit hook meant to require strict mode. Show me the inputs where it produces a false positive or false negative, then propose a hardened version. Keep it POSIX-friendly and explain each regex.” It found three edge cases I had not. This is the loop that works: I own the intent and the test cases, the model owns the tedious pattern-matching. If you want to standardize prompts like this across a team, a saved prompt pack of hook-review prompts beats everyone reinventing them, and the prompt library has starting points.
Pro Tip: feed the AI a few intentionally broken scripts and ask it to confirm your hook rejects all of them. A hook you have not tried to fool is a hook you do not trust.
Keep the hooks fast and scoped
A pre-commit hook that takes 40 seconds gets bypassed with --no-verify, and a bypassed hook protects nothing. Scope hooks to the files that changed, which pre-commit does by default, and push the slow, repo-wide stuff to CI. The blast radius of a commit-time hook should be exactly the staged diff, nothing more.
- repo: local
hooks:
- id: ansible-lint
name: ansible-lint (changed playbooks only)
entry: ansible-lint
language: python
files: '^playbooks/.*\.ya?ml$'
pass_filenames: true
When I ask an AI to optimize a config, the prompt is about constraints: “This config runs in under five seconds on a single-file commit. Suggest changes that keep it under that budget. Anything slower than that, recommend as a CI-only check instead and tell me which it should be.” Giving the model a budget keeps it from cheerfully adding a full integration suite to your commit path.
The CI half and the human gate
The same config runs in CI, which is the backstop for anyone who skipped local hooks:
# .github/workflows/pre-commit.yml
name: pre-commit
on: [pull_request]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: pre-commit/action@v3.0.1
Here is the rule I hold to no matter how good the hooks get: a failing hook blocks the merge, but a human approves the merge. Hooks catch known-bad patterns. They do not understand intent, they cannot judge whether a change is the right change, and they will happily pass code that is syntactically perfect and operationally catastrophic. The hook is a gate, not an approver. When an AI suggests a change in response to a hook failure, that suggestion goes through review like any other diff. If you want AI suggestions to be able to act rather than just advise, you need explicit approval gates around them, which I work through in ChatOps approval gates for AI-suggested actions.
That review discipline is the same one behind our code review dashboard: the machine pre-scans and flags, a person owns the call. Whichever assistant you use for the drafting, whether Claude, Copilot, or Cursor inline, the boundary does not move. It writes; you decide; the hook enforces; CI backstops.
Conclusion
Pre-commit hooks are the cheapest place in your pipeline to stop a bad change, and AI makes building them genuinely fast. Lean into that: let the model draft configs, harden your custom checks, and explain failures. Then do the part the model cannot. Read every hook before it runs, scope its blast radius to the staged diff, keep a --no-verify back-out for the rare emergency, and never hand a commit-time hook your prod credentials. The model is a fast junior engineer. Treat it like one, and your automation repo gets safer every commit. For more in this vein, browse the automation category.
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.