Skip to main content

charmlint

A charm-aware, model-agnostic linter for Juju charms.

charmlint checks charm source code against Canonical's Juju charm best practices: observability (COS integration, ops-tracing), security (PEP 740 PyPI attestations), testing structure, metadata completeness, configuration quality, and more.

Extracted from tonyandrewmeyer/cantrip, where charmlint grew as an internal component of the Cantrip AI charm builder.

Installation

uv sync --dev

Usage

uv run charmlint /path/to/charm
uv run charmlint --format json /path/to/charm
uv run charmlint --select COS,META /path/to/charm
uv run charmlint --ignore ATT002 --strict /path/to/charm

Or install and run directly:

uv run pip install -e .
charmlint /path/to/charm

Running tests

uv run pytest tests/ -v

Rule catalogue

Rule IDs follow CATEGORY-### (for example SECURITY-001, METADATA-003). See docs/id-scheme.md for the full category catalogue and naming rules.

Configuration

Configure under [tool.charmlint] in pyproject.toml, or in a standalone charmlint.toml / .charmlint.toml. Discovery walks up from the charm directory, in the manner of ruff.

[tool.charmlint]
severity = "warning"  # minimum severity to report
select = ["SECURITY", "METADATA"]
ignore = ["METADATA-002"]

[tool.charmlint.per-rule-severity]
"SECURITY-001" = "error"

Everywhere a rule is named — select, ignore, the keys of per-rule-severity, and --select / --ignore on the command line — it can be spelled three ways:

Spelling Example Means
Rule ID SECURITY-001 that one rule
Rule name secret-in-plain-config that one rule
Category SECURITY every rule in the category

Names are the wordier spelling, in the manner of ruff: ignore = ["secret-in-plain-config"] says what has been turned off without a trip to the rule catalogue. Both spellings resolve to the same rule, so a config can mix them.

A spelling that names nothing charmlint knows about is an error rather than a rule that silently never fires, so a typo is caught at startup. Only rules that exist in the running charmlint count as known: a category with no rules yet, and a well-formed ID for a rule that has not landed (OBSERVABILITY-005), are both rejected.

Case is significant. Rule IDs and categories are upper-case, rule names lower-case; security-001 names nothing.

When select and ignore disagree, the more specific spelling wins: select = ["secret-in-plain-config"] with ignore = ["SECURITY"] runs that one rule and no other security rule.

Suppressing findings inline

Individual findings can be silenced from within a charm's YAML and Python files, with ruff-style suppression comments. Codes inside a directive are the same three spellings as select and ignore accept — rule ID, rule name, or category.

# charmlint: ignore[...] suppresses the listed rules on one line. At the end of a line it covers that line; on a line of its own it covers the next line that is not blank or a comment, so a directive can sit above the thing it excuses:

config:
  options:
    admin-password:  # charmlint: ignore[SECURITY-001]
      type: string
    # charmlint: ignore[secret-in-plain-config]  # set by the operator
    api-token:
      type: string

A directive on its own line must start that line: free text belongs after it, not before, or the directive is read as trailing the comment line itself.

# charmlint: file-ignore[...], wherever it appears, suppresses the listed rules across the whole file:

# charmlint: file-ignore[CORRECTNESS-001]

Findings that anchor to a whole file rather than a line — a missing metadata field, say — can only be silenced by a file-level directive.

Suppressing everything

The code list is optional on both verbs. # charmlint: ignore suppresses every finding on the line it governs, and # charmlint: file-ignore every finding in the file:

# charmlint: file-ignore
config:
  options:
    admin-password:  # charmlint: ignore
      type: string

Scope is decided by the verb and by where the directive is written, and by nothing else: no directive silences a whole file from the end of a line of config. To silence a file, write file-ignore on a line of its own.

The bare # noqa form

In YAML, charmlint also honours the bare form it has always accepted: an inline # noqa suppresses every finding on that line, and # noqa: SECURITY-001, METADATA-002 suppresses the listed rules.

config:
  options:
    admin-password:  # noqa
      type: string

It is not honoured in Python files: there, # noqa is ruff's, and charmlint neither consumes ruff's directives nor asks a charm to write one that ruff would then report as unused. Python files take the # charmlint: forms only.

Versioning

charmlint follows a ruff-style versioning scheme: the minor version carries breaking changes (such as new or sharpened rules being enabled by default) and the patch version carries bug fixes. See docs/versioning.md for the full policy.

License

Apache 2.0 — see LICENSE.

Metadata

Release files for charmlint 0.2.1

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

Source distribution (sdist)

Source distribution for charmlint 0.2.1
File Size Uploaded
charmlint-0.2.1.tar.gz 167.3 kB Details

Built distribution (wheel)

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

Total release size: 260.7 kB

Release files / charmlint-0.2.1.tar.gz

Download URL charmlint-0.2.1.tar.gz
Size 167.3 kB
Tags Source
SHA-256 checksum
How to use checksums
14643e17ebeaee084dd468f8105522975dd3078766cc9b02b4e6f56c3f480a86
BLAKE2b-256 checksum
How to use checksums
803632f4333055378ea3e378342ce91941121c15b26d7b0af3979dc6ae26f116
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 Sep 23, 2026.

Transparency log

Release files / charmlint-0.2.1-py3-none-any.whl

Download URL charmlint-0.2.1-py3-none-any.whl
Size 93.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fc1d753de3d53a4a6e7029856e78eeb05911cecf8c125f37ec6bfbd85ad37af3
BLAKE2b-256 checksum
How to use checksums
4eb52ab28be0808a034e914f3300c80cce198707093561682fe9af66b39b49e9
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 Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

This release

0.2.1 This release

2 release files

0.1.1

2 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