Skip to main content

amlint

CI Docs Coverage PyPI Python Docker Verified against Alertmanager

A linter for Prometheus Alertmanager configs that finds the bugs nothing else complains about.

amtool check-config tells you whether your config is valid. amlint tells you whether it does what you meant: whether every alert reaches someone, whether a route can ever match, whether an inhibit rule mutes far more than you think. These configs load fine, Alertmanager runs without a single warning, and alerts quietly go to the wrong place, or nowhere.

Try it in your browser → Paste a config and see the problems, the route tree and where any alert would go. It runs entirely in the page, so your config never leaves your machine.

What it looks like

# alertmanager.yml
route:
  receiver: default
  routes:
    - receiver: slack-all            # no matchers: catches every alert
    - receiver: pager
      matchers: [severity="critical"]
inhibit_rules:
  - source_matchers: [severity="critical"]
    target_matchers: [severity="warning"]   # no `equal`
receivers:
  - name: default
    webhook_configs: [{url: "http://hooks.internal/default"}]
  - name: slack-all
    slack_configs: [{api_url: "https://hooks.slack.com/x", channel: "#alerts"}]
$ amlint check alertmanager.yml

  ✖  ERROR  [undefined-receiver]  ·  alertmanager.yml:5:7  ·  route.routes[1]
     Route references receiver 'pager' which is not defined in receivers.
     Alertmanager refuses to load this config.

  ⚠  WARN   [inhibit-no-equal]  ·  alertmanager.yml:8:5  ·  inhibit_rules[0]
     Inhibition rule has no 'equal' field: it will silence alerts across
     unrelated firing sources (no shared label binding them). Almost always a mistake.

  ⚠  WARN   [unreachable-route]  ·  alertmanager.yml:4:7  ·  route.routes[0]
     Catch-all route (no matchers) with continue:false will intercept all alerts
     — 1 subsequent sibling(s) are unreachable.

  ℹ  INFO   [global-resolve-timeout-missing]  ·  alertmanager.yml:1:1  ·  global
     global.resolve_timeout is not set, so Alertmanager uses 5m. It only applies
     to alerts sent without an end time; Prometheus always sets one.

  ✖ 1 error  ·  ⚠ 2 warnings  ·  ℹ 1 info

In plain words: the pager receiver doesn't exist, so Alertmanager won't even load this. Fix that, and the first route still catches every alert, so the critical route is never reached. And one critical alert anywhere mutes every warning everywhere, because the inhibit rule doesn't say which alerts belong together. amtool would flag only the first problem.

Every finding points to file:line:col. The exit code is 1 on any error, so CI fails.

Install

pip install amlint

Other ways:

# Docker, no Python needed
docker run --rm -v "$PWD":/cfg ghcr.io/danikdanik2013/amlint check /cfg/alertmanager.yml

# Homebrew
brew tap danikdanik2013/amlint https://github.com/danikdanik2013/amlint
brew install amlint

Or don't install anything and use the playground.

In your editor: install the amlint extension for VS Code, or set up Neovim or Helix. You see findings as you type, in the config and in its .tmpl template files, and a hover on each route shows where its alerts go.

Using an AI coding assistant? In Claude Code, run /plugin marketplace add danikdanik2013/amlint and then /plugin install amlint@amlint. Claude will then use amlint to lint your configs and answer "where does this alert go?". Other assistants can read llms.txt.

What you can do with it

I want to… Run
Find problems in a config amlint check alertmanager.yml
Fail CI on warnings too, not only errors amlint check alertmanager.yml --strict
See where an alert would go amlint route alertmanager.yml team=db severity=critical
Check whether an alert gets muted amlint route alertmanager.yml alertname=DiskFull --firing alertname=NodeDown
Write unit tests for routing amlint test alertmanager.yml tests.yml
See what a change does to routing amlint diff old.yml new.yml
Look at the route tree amlint tree alertmanager.yml
Draw the route tree as a Mermaid diagram amlint tree alertmanager.yml --format mermaid
Catch routes for alerts that don't exist amlint check alertmanager.yml --rules 'rules/*.yml'
Lint what you deploy to Kubernetes amlint check alertmanager-secret.yaml or kubectl kustomize deploy/ | amlint check -
Rewrite deprecated match: syntax as matchers: amlint fix alertmanager.yml
Learn what a check means and how to fix it amlint explain shadowed-route
See every check amlint list
See findings in VS Code, Neovim or Helix as you type amlint lsp, started by the editor (setup)

Full guide: usage docs.

What it catches

48 checks. 26 of them catch things amtool doesn't, because the config is valid and simply behaves differently than you expect, or uses syntax Alertmanager has deprecated:

  • Alerts that reach nobody. A route points at a receiver that has no integration, or a catch-all route sits above more specific ones so they never match.
  • Routes that can never match. An earlier sibling route catches every alert first. This can be an exact duplicate (route-match-collision) or a broader one, such as severity=~"warning|critical" before severity="critical" (shadowed-route).
  • Notification templates that break when they're sent. A missing }, a template name with a typo, or .Labels where the data has .CommonLabels. Alertmanager loads these configs fine; the notification fails, or Slack sends it with an empty title.
  • Inhibit rules that mute too much. No equal labels, rules that mute each other, or source and target that match the same alert.
  • Timings that contradict themselves. group_wait longer than group_interval, or repeat_interval shorter than group_interval.
  • Time intervals that never apply, or always do. An active_time_intervals whose years are over, or mute intervals that cover the whole week, so a route never sends anything.
  • Matchers Alertmanager reads differently than you do. For example, code=~[0-9]{3} without quotes loses its closing brace.
  • Routes for alerts that don't exist (with --rules). A typo like HighCpuUsage vs HighCPUUsage silently kills a branch.
  • Deprecated syntax such as match: and source_match:, which amlint fix rewrites as matchers: for you.
  • Secrets in plain text: API keys, tokens and webhook URLs written into the config instead of read from a file.

The other 22 checks repeat what amtool already rejects: undefined receivers, missing webhook URLs or API keys, bad regexes. They're there so one tool gives you one report, in one format.

All checks with examples →

Test your routing like code

Write down which alerts should go where, and let CI tell you when that stops being true:

# tests.yml
- name: critical infra alerts page infra and the on-call
  labels: {team: infra, severity: critical}
  expect: [infra-pager, pager]        # any order
- name: unknown teams fall back to the default
  labels: {team: unknown}
  expect: [default]
- name: disk alerts are muted while their node is down
  labels: {alertname: DiskFull, instance: db-1}
  firing:                             # other alerts firing at the same time
    - {alertname: NodeDown, instance: db-1}
  inhibited: true
amlint test alertmanager.yml tests.yml                  # fails with a diff if a case breaks
amlint test alertmanager.yml tests.yml --fail-under 90  # and if tests reach < 90% of routes

amlint diff old.yml new.yml works the other way round. You don't write any cases. It compares two versions and lists every kind of alert that now goes somewhere else, for example team=db, severity=critical: pagerduty-db → slack-db.

Can I trust the simulation?

amlint simulates routing and inhibition in Python. To prove it behaves like the real thing, a CI job generates thousands of configs and alerts and runs them through amtool and a running Alertmanager, v0.32.2, v0.33.1 and v0.34.1. It checks that both give the same receivers, the same inhibitions and reject the same configs. The few known differences are listed on that page.

Use it in CI

GitHub Action. Findings show up as annotations on the PR diff:

- uses: actions/checkout@v7
  with:
    fetch-depth: 0
- uses: danikdanik2013/amlint@v0.8.0
  with:
    config-path: alertmanager.yml
    strict: true
    diff-base: ${{ github.event.pull_request.base.sha }}   # optional: post a routing diff comment

With diff-base the action also posts one PR comment, updated on every push. It shows which alerts now reach different receivers and which findings are new or fixed. It needs permissions: pull-requests: write. Action docs →

Anywhere else: pip install amlint && amlint check alertmanager.yml --strict. Use --format github, --format sarif (GitHub Code Scanning), --format json, or amlint diff --format markdown for a PR comment.

pre-commit:

repos:
  - repo: https://github.com/danikdanik2013/amlint
    rev: v0.8.0
    hooks:
      - id: amlint        # runs on files ending in alertmanager.yml or alertmanager.yaml

Configure it

Turn checks off or change their severity for the whole project, in .amlint.yml or pyproject.toml:

# .amlint.yml
ignore: [empty-receiver]
strict: true
severity:
  unused-receiver: error

Or silence one spot, with the reason right next to it:

# amlint: disable-file=global-resolve-timeout-missing   ← whole file

receivers:
  - name: blackhole   # amlint: disable=empty-receiver   ← this line

Kubernetes, Helm and Mimir

Point amlint at the file you actually deploy. It detects the format and finds the Alertmanager config inside:

  • a Kubernetes Secret or ConfigMap with alertmanager.yaml
  • kube-prometheus-stack Helm values (alertmanager.config)
  • a Mimir or Cortex tenant config (alertmanager_config)
  • prometheus-operator AlertmanagerConfig resources, merged the way the operator does it

Line numbers still point into your file. Details →

How is this different from amtool?

amtool amlint
Rejects invalid configs (undefined receiver, bad regex, missing API key) ✅ ✅
Finds valid configs that misroute or over-mute alerts — ✅
Where does one alert go? ✅ ✅
A file of routing tests, run in CI, with route coverage — ✅
Inhibition: is this alert muted while that one fires? — ✅
What does this change do to routing? — ✅
Kubernetes / Helm / Mimir / AlertmanagerConfig input — ✅
PR annotations, SARIF, JSON, Markdown — ✅
Needs a Go binary ✅ —

They work well together. If you already run amtool check-config in CI, keep it, and add amlint for everything after "is it valid?".

Metadata

Release files for amlint 0.8.0

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

Source distribution (sdist)

Source distribution for amlint 0.8.0
File Size Uploaded Provenance
amlint-0.8.0.tar.gz 137.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for amlint 0.8.0
File Interpreter ABI Platform
amlint-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 278.6 kB

Release files / amlint-0.8.0.tar.gz

Download URL amlint-0.8.0.tar.gz
Size 137.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5d8339e620518bc03062dc25749c21d178c44ac5cb4a53089e9385a2d4bd15f6
BLAKE2b-256 checksum
How to use checksums
2701ef48234b9d3c12fcda939cca4699e323281a573e5a3e8fd92ab3790805db
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 20, 2026.

Transparency log

Release files / amlint-0.8.0-py3-none-any.whl

Download URL amlint-0.8.0-py3-none-any.whl
Size 141.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
84ba5c65f4b2131294a630915f1c77bdb4be9e24560c1d17fda53e3036e4f51a
BLAKE2b-256 checksum
How to use checksums
f7a79134d734fbdf4c6ebb25a69549e14c81a4fdf1a00774fcfe78f4d256b673
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 20, 2026.

Transparency log

Security for amlint 0.8.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for amlint-0.8.0.tar.gz

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 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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