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)
| File | Size | Uploaded | |
|---|---|---|---|
| charmlint-0.2.1.tar.gz | 167.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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