#15038·algo

Proposal: use ansible-static-lint in the pre-commit hook (526x faster, identical findings)

Author: arhumanCreated Aug 28, 2026Updated Aug 28, 2026

Summary

The ansible-lint pre-commit hook takes about 13 seconds on every commit that touches a YAML file. I maintain ansible-static-lint (astl), a Go reimplementation of ansible-lint's static rules that aims for byte-identical output, and I benchmarked both tools on this repository (HEAD 20e22a8). I would like to propose using it in the pre-commit hook, while keeping ansible-lint in CI for full coverage.

Measurements

Same tree, same machine, hyperfine, 5 runs, warmup 1. ansible-lint 25.8.2 (the version uv run --with ansible-lint resolves under ansible==12.3.0), full-tree invocation (240 files):

tool mean time
ansible-lint (no-arg form, 240 files) 13.128 s ± 0.043
astl 25.0 ms ± 0.3

That is a 526x speedup, and it turns the hook from a noticeable pause into something below the perception threshold. The 13 s figure is a floor for the real hook, since uv run adds its own environment resolution on top.

Findings parity

On the current tree, with .ansible-lint and .yamllint honoured (exclude_paths, skip_list, enable_list, mock_modules), both tools report zero findings on the full 240-file set. To verify that the double zero is real agreement and not a discovery gap, I also ran both tools with the .ansible-lint config removed: 270 findings match byte for byte (name[missing], var-naming, name[casing], role-name, no-handler, key-order, no-changed-when). The remaining differences are rules astl deliberately does not port (fqcn, syntax-check, args, jinja, no-free-form) and a handful of lines that differ between ansible-lint 25.8.2 and 26.8.0 themselves.

Scope caveat, stated plainly: astl covers ansible-lint's static rules only. It does not run syntax-check or the fqcn/args/jinja families. That is why the proposal is to swap the pre-commit hook, where latency matters on every commit, and keep ansible-lint in CI as the authoritative gate.

A note on the current lint setup

While measuring, I noticed the current gates may not be checking what they appear to:

  • The CI step runs uv run --with ansible-lint ansible-lint .. With an explicit . argument, ansible-lint only examines what its kind globs match at the top level: 6 files, of which 3 are the lint configs themselves. Your own green CI log shows it ("Passed: 0 failure(s), 0 warning(s) on 6 files"). The no-argument form discovers 240 files. Dropping the . makes the CI gate actually lint the tree.
  • The pre-commit entry is bash -c 'uv run ansible-lint --force-color || echo "..."', so the || echo swallows the exit code and the hook can never fail a commit.

The tree is clean either way (I verified the 240-file run is green), so nothing is currently slipping through, but both gates would report success even if it were not.

Proposed change

yaml
- repo: https://github.com/arhuman/ansible-static-lint
  rev: v0.3.0
  hooks:
    - id: astl

pre-commit installs astl in an isolated environment and caches it; with pre-commit 3.0 or newer, neither astl nor Go needs to be installed beforehand. The first run bootstraps Go and compiles once, subsequent runs use the cached binary. astl reads the repository's .ansible-lint and .yamllint files, including exclude_paths, skip_list, enable_list, profile and warn_list, so no configuration change is needed.

Happy to open a PR if there is interest.