Skip to main content

ansible-static-lint

CI Release Go Reference License: MIT KwalitAI score

A fast, offline static companion to ansible-lint, for editor-on-save, pre-commit, or a CI pre-check. No Ansible runtime, no Python, no environment setup, near-instant feedback. The command is astl.

Same config file. Same rule ids. Same # noqa comments. Same output.

astl is a Go reimplementation of the ansible-lint rules that can be decided from the YAML source alone: 40 of the 51 built-in rule IDs, reproducing ansible-lint's -f pep8 output byte for byte within that scope. It is not a drop-in replacement for ansible-lint and does not try to become one: keep ansible-lint where its runtime matters, for syntax check, collection resolution, schema validation and --fix.

astl
Runtime dependencies none, one static binary
ansible-lint rules supported 40 of 51
Output conformance within that scope 2417 / 2417 findings, byte for byte
478-file corpus 45.6 ms, against 33.5 s

Try it on your repository

Download the binary. No Go, no Python, no Ansible:

VERSION=0.7.1
OS=$(uname -s | tr 'A-Z' 'a-z')
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
curl -sSfL "https://github.com/arhuman/ansible-static-lint/releases/download/v${VERSION}/ansible-static-lint_${VERSION}_${OS}_${ARCH}.tar.gz" | tar -xz astl
./astl path/to/playbooks

Linux, macOS and Windows, amd64 and arm64, are on the releases page. Each release ships checksums signed with cosign; see docs/supply-chain.md to verify one.

With Homebrew, on macOS or Linux:

brew tap arhuman/tap
brew install ansible-static-lint

From v0.7.0 the tap ships a cask, generated from the release archives on every tag so it cannot lag behind a release. Earlier versions ship a formula; brew install resolves whichever the tap currently holds, and an existing formula install migrates to the cask on the next brew update.

From PyPI, if you already manage ansible-lint with pip or pipx:

pipx install ansible-static-lint
astl path/to/playbooks

The wheel carries the same binary as the release archive above, so there is still no Python at runtime: no interpreter starts when you run astl. Python is only the delivery mechanism. pip install ansible-static-lint works too, in a virtualenv.

In GitHub Actions. Two lines, and the action fetches and checksum-verifies the binary for you:

      - uses: actions/checkout@v7
      - uses: arhuman/ansible-static-lint@v0.7.1

As a pre-commit hook. 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: language: golang bootstraps the toolchain itself. The first run downloads Go and compiles astl once; every run after that uses the cached binary.

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

With Go already installed, skip the download: go install github.com/arhuman/ansible-static-lint/cmd/astl@latest. Go 1.26 or newer.

None of these ask you to change your repository. If it already has an ansible-lint config file, astl reads it as is: profile, skip_list, enable_list, warn_list and exclude_paths work unchanged, and so do your existing # noqa comments. More in docs/configuration.md.

Ansible linting on Windows, without WSL or Ansible

ansible-lint does not target Windows: it ships Operating System :: MacOS and :: POSIX classifiers and depends on ansible-core, whose control node is POSIX-only. The usual answer is WSL, a container, or a Linux CI runner. astl needs no runtime at all, so a native Windows binary lints the same playbooks from PowerShell directly.

$TAG = "v0.7.1"
$VERSION = $TAG.TrimStart("v")
Invoke-WebRequest "https://github.com/arhuman/ansible-static-lint/releases/download/$TAG/ansible-static-lint_${VERSION}_windows_amd64.zip" -OutFile astl.zip
Expand-Archive astl.zip -DestinationPath .
.\astl.exe path\to\playbooks

pipx install ansible-static-lint works the same way it does elsewhere, and still starts no interpreter at run time. Windows wheels ship for amd64 and arm64.

In GitHub Actions the action runs on Windows runners too:

runs-on: windows-latest
steps:
  - uses: actions/checkout@v7
  - uses: arhuman/ansible-static-lint@v0.7.1

What you do not need: WSL, a Linux container, a Python interpreter, ansible-core, or a Visual C++ redistributable. The binary is statically linked and astl.exe is the whole install.

The rules are the same 40 on every platform. Paths in the output use the separator the platform reports, and a repository's .ansible-lint config is read identically.

Why static?

astl has no Ansible runtime and never shells out. It therefore does not:

  • run ansible-playbook --syntax-check;
  • resolve collections, roles or module argument specs;
  • evaluate Jinja templates;
  • validate content against JSON schemas;
  • apply --fix transforms;
  • load custom rule plugins or profiles.
astl ansible-lint
The 40 statically decidable rules yes yes
The 11 runtime-dependent rules no yes
--fix no yes
Needs Python and an Ansible install no yes
Cold start 2.9 ms 0.37 s

That is the trade-off: astl handles every check that can be decided from the source alone and leaves runtime-dependent validation to ansible-lint. The boundary is measured, not rhetorical: 99% of the corpus findings astl does not report require capabilities it deliberately does not have, and the full per-rule accounting is in docs/scope.md.

The flip side of having no runtime: astl keeps linting files whose references Ansible cannot resolve, where ansible-lint abandons the file and reports only the load failure.

Compatibility

Two numbers, and they measure different things:

Metric Value
Conformance within the supported scope 2417 / 2417 golden findings reproduced (100%)
Coverage of all ansible-lint findings on the corpus 2417 / 2648 (91.3%)

Within its scope, astl agrees with ansible-lint on every finding: same file, same line, same column, same message, byte for byte. It also emits 55 findings ansible-lint does not, all on files ansible-lint abandons because its embedded runtime rejects them. Those extras are pinned line for line as an exact set: the harness fails if the set changes in either direction, so a new false positive cannot hide among them.

The corpus, the frozen golden output, and the assertion live in a separate repository, astl-compatibility-check, kept as a sibling directory. It is licensed GPL-3.0-or-later because it contains ansible-lint test data. astl carries neither the corpus nor the golden output into its own tree and stays MIT. What astl does carry, under its default output mode, is ansible-lint's diagnostic message text itself, reproduced verbatim for compatibility; see ADR 0004 for the reasoning and its limits.

How fast?

Measured on Apple Silicon macOS against ansible-lint 26.8.0 (Python 3.14, --offline), on ansible-lint's own examples corpus. make bench-compare in the compatibility repository reproduces every row:

Metric ansible-lint astl (40 rules)
Cold start (--version) 0.37 s 2.9 ms
One 6-line playbook 1.5 s 3.3 ms
478-file corpus 33.5 s 45.6 ms
Max RSS on the corpus 130 MiB 42 MiB

Read the ratios with care: the comparison is asymmetric, since ansible-lint is also running its syntax-check subprocess and the 11 rules astl excludes. The honest headline numbers are cold start and the single playbook, where the gap is interpreter and import overhead that exists before any rule runs; even ansible-lint --version costs half a second.

make bench fails the build if linting the reference corpus exceeds 150 ms, so the property is guarded rather than assumed. That guard is one-sided by design: it times astl alone. The two-linter comparison above is make bench-compare. Why the numbers look like this is in docs/performance.md.

Using astl in CI

astl is not a drop-in replacement for ansible-lint, so the pipeline that gets the most out of it runs both, at different frequencies:

Tier When What runs Why
Fast every push and pull request astl seconds, and it covers the 40 rules that block most pipelines
Deep merge to the default branch, or nightly ansible-lint the 11 runtime-dependent rules astl cannot decide

Adopting the fast tier costs no configuration, since --ids upstream is the default. docs/ci.md has the full workflows: action inputs, SARIF upload to GitHub code scanning, and the pre-commit variants.

Usage

astl path/to/playbooks     # pep8 lines on stdout
astl -f sarif .            # SARIF 2.1.0 instead, see docs/sarif.md
astl -c ci.ansible-lint .  # lint against a named config

astl follows include_tasks, import_tasks, include and import_playbook to a fixpoint, so a task list is linted as tasks even when it does not sit under a tasks/ directory. A file reached only through an include is linted; a file reached both by the directory walk and through an include is linted under both kinds, and the duplicate findings that produces are removed. Templated targets such as {{ role_path }}/x.yml are left alone, since resolving them needs the Ansible runtime astl deliberately does not have.

Exit code Meaning
0 clean, no violations
1 usage or runtime error, for example an input path that does not exist
2 violations were found
3 the run could not check every file it was given

Exit 3 says a file astl was given could not be examined, so there is no result for it. It takes precedence over 2, because a violation is a result you can read while an unchecked file is not, and an exit code is the only place that can say so. docs/exit-codes.md has the details, including what does not count as unchecked.

Rules

40 static rules are supported; the full equivalence table between astl's domain.rule[tag] identifiers and ansible-lint's is in docs/rules.md. internal/rules/ids.go is the single source both taxonomies are derived from.

--ids native switches output to the native vocabulary: both the rule identifier and the message wording, in pep8 and SARIF alike. The default is --ids upstream, which keeps the pep8 output byte for byte identical to ansible-lint's. The SARIF document is not held to that comparison and does not try to match upstream's own SARIF, for the reasons in ADR 0007. It also declares its run's working directory and which rules were enabled for it; see docs/sarif.md.

# default, ansible-lint's identifier and wording
playbooks/demo.yml:11: no-changed-when: Commands should not change things if nothing needs doing.

# --ids native
playbooks/demo.yml:11: task.unguarded-change: This command always reports changed. Add changed_when, or a creates guard.

The native messages state the defect, then the fix. They are original text, never a reworded copy of upstream's, and they stay under 100 characters so that the fix survives truncation in CI annotations and editor panels.

Early feedback wanted

astl is early. I am looking for real-world Ansible repositories to test whether the static-only trade-off is useful outside the compatibility corpus. If you try it, please open an issue with anything surprising: missing findings, extra findings, unsupported workflows, or simply why you would not use it.

Verifying the implementation yourself

From nothing to a verified binary in three commands, needing neither Python, ansible-lint, nor the test corpus:

git clone https://github.com/arhuman/ansible-static-lint
cd ansible-static-lint
make check

make check builds bin/astl, lints the deliberately broken playbook in examples/, and asserts the run against examples/expected.txt. A working build ends on:

==> ./bin/astl examples
examples/playbook.yml:4:3: name[play][/]: All plays should be named.
examples/playbook.yml:6: command-instead-of-shell: Use shell only when shell functionality is required.
examples/playbook.yml:6: name[missing][/]: All tasks should be named.
examples/playbook.yml:6: no-changed-when: Commands should not change things if nothing needs doing.
examples/playbook.yml:7:13: name[casing][/]: All names should start with an uppercase letter.
examples/playbook.yml:7: package-latest: Package installs should not use latest.
examples/playbook.yml:11: risky-file-permissions: File permissions unset or incorrect.
OK: 7 findings, exit code 2, output matches examples/expected.txt

Anything else is a failure with a diff, because a linter that runs and reports nothing looks exactly like a linter that runs and is broken. The findings and the exit code are both asserted, so neither can drift unnoticed. examples/playbook.yml is 11 lines of YAML and trips seven rules across five families, so it also doubles as the shortest tour of what astl catches.

Development

make check    # build + lint examples/, asserted against examples/expected.txt
make test     # go test -race ./...
make cover    # tests with coverage, fails below COVER_MIN
make audit    # coverage gate + go mod verify + golangci-lint + govulncheck
make parity   # pep8 output parity against the frozen ansible-lint corpus
make bench    # both speed guards: noqa shape, then the corpus under 150 ms
make ci       # tidy + audit + workflows + check + parity + bench, the gate before any commit
make tidy     # gofmt + go mod tidy

make test cannot see a compatibility regression: it asserts behaviour, not byte-exact output. Anything touching a rule, a message, or a finding's line or column needs make parity, which needs the sibling repository described above. make ci runs both. See CONTRIBUTING.md.

License

MIT, see LICENSE.

Metadata

Release files for ansible-static-lint 0.7.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for ansible-static-lint 0.7.1
File
ansible_static_lint-0.7.1-py3-none-win_arm64.whl Python 3 none Windows ARM64 Details
ansible_static_lint-0.7.1-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
ansible_static_lint-0.7.1-py3-none-musllinux_1_2_x86_64.whl Python 3 none Linux musl 1.2+ x86-64 Details
ansible_static_lint-0.7.1-py3-none-musllinux_1_2_aarch64.whl Python 3 none Linux musl 1.2+ ARM64 Details
ansible_static_lint-0.7.1-py3-none-manylinux_2_17_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details
ansible_static_lint-0.7.1-py3-none-manylinux_2_17_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64 Details
ansible_static_lint-0.7.1-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
ansible_static_lint-0.7.1-py3-none-macosx_10_9_x86_64.whl Python 3 none macOS 10.9+ x86-64 Details

Total release size: 13.6 MB

Release files / ansible_static_lint-0.7.1-py3-none-win_arm64.whl

Download URL ansible_static_lint-0.7.1-py3-none-win_arm64.whl
Size 1.6 MB
Tags Python 3 Windows ARM64
SHA-256 checksum
How to use checksums
a370f5df92f6ab06d202cfd00da967a2d3d07297db72e3978db84eeb03d86440
BLAKE2b-256 checksum
How to use checksums
2280e5e77dfef797ca9b1d8dc15b1a0cedeb09c29c47ba980237cd8f221c3f68
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / ansible_static_lint-0.7.1-py3-none-win_amd64.whl

Download URL ansible_static_lint-0.7.1-py3-none-win_amd64.whl
Size 1.8 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
47886a650eeab8e60103fe9361613dd12ebe9225ccae8104d9126a5c6a58b98f
BLAKE2b-256 checksum
How to use checksums
89c57c225b3878fc47460569a7c977483bfe971df7c53daaecf54fd00d4ac545
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / ansible_static_lint-0.7.1-py3-none-musllinux_1_2_x86_64.whl

Download URL ansible_static_lint-0.7.1-py3-none-musllinux_1_2_x86_64.whl
Size 1.8 MB
Tags Linux musl 1.2+ x86-64 Python 3
SHA-256 checksum
How to use checksums
f74ce13e09b968910310dcd67d70170f53e73244a6b5be2b687475ca501812af
BLAKE2b-256 checksum
How to use checksums
e96a498f309babddce19a160f04613f6040ee83b0d6643d40bba3fe01c0f8dbf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / ansible_static_lint-0.7.1-py3-none-musllinux_1_2_aarch64.whl

Download URL ansible_static_lint-0.7.1-py3-none-musllinux_1_2_aarch64.whl
Size 1.6 MB
Tags Linux musl 1.2+ ARM64 Python 3
SHA-256 checksum
How to use checksums
273b469eefe21491168dd0fd9a516d4dfe1c47aa8e668f83c8c8465034204782
BLAKE2b-256 checksum
How to use checksums
59c71484bd8373343c373101c5ee80433015b1e19eaf6423badc1f29257194a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / ansible_static_lint-0.7.1-py3-none-manylinux_2_17_x86_64.whl

Download URL ansible_static_lint-0.7.1-py3-none-manylinux_2_17_x86_64.whl
Size 1.8 MB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
532fa506dc20dce82397f4167659adf834ce5fa2e93d4ece1c4e0c8e39c82f1a
BLAKE2b-256 checksum
How to use checksums
85e23fa7d03931e8f8e07a1f78c69f0975c6b03c52b5e0acf87a4f67bd295b48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / ansible_static_lint-0.7.1-py3-none-manylinux_2_17_aarch64.whl

Download URL ansible_static_lint-0.7.1-py3-none-manylinux_2_17_aarch64.whl
Size 1.6 MB
Tags Linux glibc 2.17+ ARM64 Python 3
SHA-256 checksum
How to use checksums
7fc0ff8e16d3dfa92917eca4590b60845f3bbb6a70bf70966dae924734adb4a4
BLAKE2b-256 checksum
How to use checksums
52975381c6d4a3f092291b2f29c8c4438208cf0d8b8ee2e6e656cd6b9f89de48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / ansible_static_lint-0.7.1-py3-none-macosx_11_0_arm64.whl

Download URL ansible_static_lint-0.7.1-py3-none-macosx_11_0_arm64.whl
Size 1.6 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
d2baad26eb7d7ccb1027a45ee6ed1c7ac3e058516eab1bc44270ee8f9a2530ec
BLAKE2b-256 checksum
How to use checksums
acd3188296a0dccbb0ed463645341680bf5e6d1214441899a72f012be176f81e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / ansible_static_lint-0.7.1-py3-none-macosx_10_9_x86_64.whl

Download URL ansible_static_lint-0.7.1-py3-none-macosx_10_9_x86_64.whl
Size 1.8 MB
Tags Python 3 macOS 10.9+ x86-64
SHA-256 checksum
How to use checksums
05944b9843acbe791a4e472c66d303d47685ae84bb1eeaba7194284406a20c2f
BLAKE2b-256 checksum
How to use checksums
c8665c551a027906d46b99254e2ebf7c0142ae92be5afc24d276bdffc19b0ef9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.1 This release

8 release files

0.7.0

8 release files

0.6.0

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page