Codedecorum
Codedecorum is a configurable, extensible, cross-language command-line linter that keeps AI-generated code on the rails. Repositories choose their rules; violations fail the check with location breadcrumbs and a configurable message.
Pygments provides broad lexical tokens, while Tree-sitter grammars downloaded and cached on demand provide lazy concrete syntax trees for semantic rules. A language's first semantic check for each language-pack version requires network access; later runs reuse its cached grammar. Download or loading failures stop linting rather than silently disabling semantic rules.
Table of Contents
Bundled rules
CD001rejects standalone and multiline comments outside an optional contiguous file header (up to 1,000 characters by default). Excludes directives such as shebangs and linter suppressions. Options:allow-file-header(bool) andmax-file-header-chars(int).CD002rejects trailing comments longer than 60 characters by default. Excludes directives such as shebangs and linter suppressions. Option:max-length(int).CD003rejects Pythonpassstatements,TODOmarkers in comments or identifiers, andthrowstatements whose message or exception type marks them as unimplemented placeholders.CD004rejects pytest/unittest-style Python test functions, Python doctests, and common JavaScript/TypeScript test blocks in implementation files. Recognized test files and Storybook story files remain valid separate artifacts.CD005flags every test file included by the active scope.CD006rejects changed files matching configured root-relative, case-sensitive path patterns unless that rule-path pair has a current temporary unlock. Option:paths(array of non-empty glob patterns, default[]).
Configure Lefthook
Lefthook invokes Codedecorum across the repository before each commit:
pre-commit:
commands:
codedecorum:
run: uv run codedecorum .
Configuration
Resolution order
Codedecorum selects one configuration in this order:
- A file passed with
--config .codedecorum.tomlin the repository rootcodedecorum.tomlin the repository root- Exactly one configured project manifest:
[tool.codedecorum]inpyproject.toml,"codedecorum"inpackage.json, or one of[workspace.metadata.codedecorum]and[package.metadata.codedecorum]inCargo.toml. Multiple configured project manifests—or both Cargo tables—produce an ambiguity error. - Global configuration at
$XDG_CONFIG_HOME/codedecorum/codedecorum.toml,~/.config/codedecorum/codedecorum.tomlwhenXDG_CONFIG_HOMEis unset, or%APPDATA%/codedecorum/codedecorum.tomlon Windows - Built-in defaults: all rules enabled, and default values for rules.
Available settings
scope:"diff-lines"(default),"diff-files", or"full"base: the Git revision used by diff scopes; defaults to"HEAD"include: case-sensitive glob patterns that force matching paths into source checks; defaults to[]exclude: case-sensitive glob patterns omitted from source checks; defaults to[]plugin-dirs: directories containing rule plugins; defaults to[]rules: per-rule overrides; defaults to{}, meaning all loaded rules remain enabled with their built-in options. Setenabled = falseto disable a rule, or set one of its options to replace that option's default
Note: Path patterns use root-relative POSIX paths, and inclusion overrides exclusion. Plugin directories expand ~; relative directories resolve from the configuration file's directory.
Standalone TOML
Use this format in .codedecorum.toml, codedecorum.toml, or the global configuration file:
scope = "diff-lines"
base = "HEAD"
include = ["src/**", "tests/**"]
exclude = ["vendor/**"]
plugin-dirs = ["~/company-rules"]
[rules.CD001]
enabled = false
pyproject.toml
Place the same settings under [tool.codedecorum]:
[tool.codedecorum]
scope = "diff-lines"
base = "HEAD"
include = ["src/**", "tests/**"]
exclude = ["vendor/**"]
plugin-dirs = ["./my-ai-rails"]
[tool.codedecorum.rules.CD001]
enabled = true
max-file-header-chars = 2000
Cargo.toml
For a workspace, place the settings under [workspace.metadata.codedecorum]:
[workspace.metadata.codedecorum]
scope = "diff-lines"
base = "HEAD"
include = ["src/**", "tests/**"]
exclude = ["vendor/**"]
[workspace.metadata.codedecorum.rules.CD001]
enabled = true
max-file-header-chars = 2000
For a package, use [package.metadata.codedecorum] instead:
[package.metadata.codedecorum]
scope = "diff-lines"
base = "HEAD"
include = ["src/**", "tests/**"]
exclude = ["vendor/**"]
plugin-dirs = ["~/company-rules"]
[package.metadata.codedecorum.rules.CD001]
max-file-header-chars = 2000
package.json
Use the same kebab-case setting names under "codedecorum":
{
"codedecorum": {
"scope": "diff-lines",
"base": "HEAD",
"include": ["src/**", "tests/**"],
"exclude": ["vendor/**"],
"plugin-dirs": ["./company-rules"],
"rules": {
"CD001": {
"enabled": false
}
}
}
}
Explicit file
Pass a configuration file directly to bypass discovery:
codedecorum --config ~/.config/codedecorum/codedecorum.toml .
CLI flags
Command-line flags select or override configuration for one run:
--config PATH: use a specific configuration file instead of discovery--scope {diff-lines,diff-files,full}: override the configured scope--base REVISION: override the Git revision used by diff scopes--disable RULE: disable a configured or bundled rule; repeat for additional rules--unlocking-with-explicit-human-approval RULE PATH: temporarily ignore one rule for one file;--approval-timeout DURATIONoverrides the five-minute default--version: print the installed version and exitpaths: check these files or directories; defaults to the current directory
Paths must belong to one Git repository. Rule-specific options remain config-only.
A command can combine all applicable options:
codedecorum \
--config ~/.config/codedecorum/codedecorum.toml \
--scope diff-files \
--base origin/main \
--disable CD001 \
--disable DOC001 \
src tests
Exit status is 0 when checks pass, 1 when rules report violations, and 2 for configuration or operational errors.
Suppressions
Put # codedecorum: ignore CD004 CD005 (or the // equivalent) on the first physical line to permanently suppress exact rules, or use # codedecorum: ignore all to skip the file entirely; unknown codes are ignored, while wildcards plus inline or range suppressions are unsupported.
For a temporary rule-and-file suppression, run codedecorum --unlocking-with-explicit-human-approval RULE PATH; it expires after 5m unless --approval-timeout DURATION is supplied.
How to write a plugin
Each immediate non-private .py file in a configured plugin directory exports one RULE. Plugin directories are non-recursive. Plugins are trusted Python and execute inside the Codedecorum process.
Scopes and rule units
LINE, FILE, and PATH are part of a rule's definition. A rule declares the smallest unit that can be checked without hiding violations:
LINEreports source ranges whose validity depends only on those lines. Indiff-lines, only findings intersecting changed new-side lines survive. This fits forbidden calls, local syntax policies, and bounded trailing comments.FILEreceives complete source whenever its file is in scope. This fits policies where an unchanged line can become invalid because code elsewhere changed, including file headers, imports, declarations, or relationships within one file.PATHreceives a root-relativePurePosixPathwithout reading the file. This fits naming, extension, ownership, and directory-layout policies. Path rules see files excluded from source-content checks.
changed_only=True limits a rule to changed instances of its unit even if scope is diff-files or full: changed lines for LINE, whole changed files for FILE, and changed paths for PATH.
The configured scope determines which units run. diff-lines is the default: it filters line findings to changed lines while promoting file and path rules to changed files. diff-files emits every finding from changed files. full checks the complete Git-visible inventory.
Diff scopes compare the working tree with git merge-base <base> HEAD, including branch commits, staged changes, unstaged changes, and untracked files. Deleted files are omitted and renamed files use their destination path. HEAD is the default base; CI should name its target branch explicitly.
Git standard exclusions control inventory. Tracked files remain visible if a new ignore pattern matches them, ignored untracked files stay hidden, and explicitly named files bypass ignore discovery.
from collections.abc import Iterable, Mapping
from fnmatch import fnmatchcase
from pathlib import PurePosixPath
from codedecorum.api import Finding, Option, Rule, RuleUnit
def string_patterns(value: object) -> bool:
return isinstance(value, (list, tuple)) and all(
isinstance(pattern, str) and pattern for pattern in value
)
def check(context: object, options: Mapping[str, object]) -> Iterable[Finding]:
if not isinstance(context, PurePosixPath) or context.suffix != ".md":
return
path = context.as_posix()
if not any(fnmatchcase(path, pattern) for pattern in options["allowed-paths"]):
yield Finding(detail="Markdown belongs under docs")
RULE = Rule(
code="DOC001",
name="Markdown location",
guidance="Keep Markdown under the configured documentation paths.",
check=check,
options={"allowed-paths": Option(("docs/*.md",), string_patterns)},
unit=RuleUnit.PATH,
)
Every rule provides a stable code, name, guidance, check function, semantic unit, and optional validated settings. Guidance may be a non-empty string or a callable that receives the resolved option mapping and returns a non-empty string.
guidance = lambda options: f"Keep lines within {options['max-length']} characters."
LINE and FILE checks receive a lazy SourceContext; PATH checks receive PurePosixPath. Checks yield Finding values. Line rules require complete 1-based, end-exclusive source ranges. File rules may also yield file-level findings, while path rules yield only path-level findings.
SourceContext exposes the root-relative path, lazy source text, position conversion, and a cached tuple of SyntaxToken(offset, end_offset, type, value) values. offset and end_offset are zero-based, end-exclusive character offsets into the physical source, including embedded languages; value is the exact source text, and type is a Pygments token type for comparison with families from pygments.token—for example, SyntaxToken(0, 4, Token.Keyword, "pass"). Unsupported filenames and lexers that do not return the source losslessly produce no tokens. Semantic rules can instead access context.syntax, which detects the language and lazily downloads, caches, and parses its Tree-sitter grammar before returning a cached SyntaxContext with the language, syntax root, exact node text, and syntax.finding(node) source-range conversion; unsupported languages return None. The engine validates plugin declarations and findings, attaches path and source metadata, and groups diagnostics by file and rule.
License
This repository is licensed under the MIT License.
Metadata
Release files for codedecorum 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| codedecorum-0.1.0.tar.gz | 24.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| codedecorum-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.4 kB
Release files / codedecorum-0.1.0.tar.gz
| Download URL | codedecorum-0.1.0.tar.gz |
|---|---|
| Size | 24.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
693a8bb23ab2cdca8c31b4228941ff83d90ef81e871819c5b8761a4719d803d3
|
|
BLAKE2b-256 checksum How to use checksums |
bec7d0437453cd0882f948aa8b61d2afb542bcc8c03a482e9d287eb36b0807c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / codedecorum-0.1.0-py3-none-any.whl
| Download URL | codedecorum-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6b1f21d7233b19b5f6c2f3304ae0fc16c6c82a60c44c433e7d98bd09567ecf20
|
|
BLAKE2b-256 checksum How to use checksums |
6a1ff59725f4179dff2ea2f167e43f796c1c9f385c8e975b50564c66ccb48cad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|