Git pre-commit validation for Ansible playbooks
Malformed Ansible YAML should be stopped before it reaches review. Setting up a Git pre-commit hook for Ansible syntax validation gives developers an immediate check for broken indentation, invalid playbook structure and bad task definitions while the change is still fresh.
The hook runs when git commit is attempted and can reject the commit with a non-zero exit code. It is a convenience guardrail rather than a security boundary: a developer can skip it with --no-verify, so continuous integration must remain the final authority.
Australian infrastructure teams commonly span Sydney, Melbourne, Brisbane and Perth. Melbourne and Sydney shift between UTC+10 and UTC+11 during daylight saving, while Perth stays on UTC+8, so a late-afternoon commit can otherwise wait hours for another team member to notice a broken playbook.
Many Australian enterprises run VMware estates alongside workloads in the public-cloud regions in Melbourne and Sydney. Critical infrastructure operators covered by the Security of Critical Assets Act 2018, along with organisations pursuing ASD Essential Eight alignment, need traceable controls, although a local hook alone cannot provide audit evidence. Version-controlled hooks, CI logs and protected branches work together.
Define the validation boundary
Syntax checking is not execution. ansible-playbook --syntax-check parses a playbook and resolves basic structure without running tasks, while ansible-lint applies Ansible-specific rules and catches many malformed YAML documents, including role content. Neither proves that inventories exist, credentials work, hosts respond or handlers complete successfully.
Choose a deliberate scope before writing the hook. Restricting it to playbooks/ and roles/ avoids treating unrelated GitLab CI or Kubernetes YAML as Ansible. As Jonathan's background shows, infrastructure repositories often combine several configuration formats, making careful path selection useful.
Prepare a consistent toolchain
Install the validator in a virtual environment so every workstation uses the same supported Ansible release:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install ansible-core ansible-lint
ansible --version
Record the tested versions and required collections in the repository, then install them during onboarding. On macOS and Linux, activate the environment with . .venv/bin/activate; in PowerShell use .venv\Scripts\Activate.ps1. Ansible control nodes should run on Linux, macOS or under WSL2 rather than native Windows.
A GUI Git client may not inherit an activated environment. Make .venv/ ignored, and make the hook locate its tools reliably in PATH or call .venv/bin/ansible-lint directly. Do not rely on a globally installed package whose version differs between a laptop and an Australian office desktop.
Install a tracked hook
Hooks inside .git/hooks/ are not tracked by Git. Put the script in .githooks/ instead, make it executable, and point core.hooksPath at that directory:
mkdir -p .githooks
cat > .githooks/pre-commit <<'HOOK'
#!/usr/bin/env bash
set -euo pipefail
for tool in ansible-playbook ansible-lint; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf '%s is not available in PATH.\n' "$tool" >&2
exit 1
fi
done
while IFS= read -r file; do
case "$file" in
playbooks/*.yml|playbooks/*.yaml)
ansible-playbook --syntax-check "$file"
;;
roles/*.yml|roles/*.yaml)
ansible-lint "$file"
;;
esac
done < <(git diff --cached --name-only --diff-filter=ACMR --)
HOOK
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
git add .githooks/pre-commit
The sample layout checks staged playbooks with ansible-playbook and staged role YAML with ansible-lint; adjust the case patterns to match your repository. The ACMR diff filter covers added, copied, modified and renamed files while ignoring deletions. Since the file list comes from the index, keep unstaged edits from confusing the result.
Add .githooks/pre-commit to Git, but remember that local configuration is not cloned. Include the git config core.hooksPath .githooks command in onboarding, or in a tracked bootstrap script, so teammates do not receive the script without activating it.
Test across developer workstations
Test the hook on each platform used by the team. On macOS and Linux, confirm the executable bit survived cloning and that the file uses LF line endings. On Windows, run the control node in WSL2 and invoke Git from that same environment so paths and the Python environment agree.
Add .githooks/pre-commit text eol=lf to .gitattributes, then stage a valid playbook and a deliberately broken test fixture. Confirm that the valid change passes, the malformed YAML fails, a deleted file is ignored, and an unstaged playbook does not trigger the hook.
Operational automation can fail after a clean commit as well. When an Ansible workflow manages vCenter, teams should keep the certificate expiry guide nearby because valid syntax still cannot overcome an expired certificate or unavailable endpoint.
Enforce the same rules in CI
CI needs a repository-wide check because the pre-commit script considers staged paths. Run ansible-lint across Ansible directories and run ansible-playbook --syntax-check for each entry-point playbook, then make that job a required status check. Branch protection and reviewer approval provide stronger governance than a hook any developer can bypass.
Examples published on this site are useful patterns, not controlled production procedures; review the site disclaimer before reusing them. Keep secrets out of hook output, pin collection versions, and record CI results where Australian regulated or critical-infrastructure teams can retain them for review.
Put the hook into one active repository now: add the tracked script, register .githooks, stage the broken test fixture, and confirm the commit is blocked. Give each new developer the bootstrap command and make the successful validation result part of the team’s normal workflow.
Run the check locally, repeat it in CI, and keep the server-side pipeline as the gate before Ansible reaches production.