Skip to main content

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

  1. Bundled rules
  2. Configure Lefthook
  3. Configuration
  4. Suppressions
  5. How to write a plugin
  6. License

Bundled rules

  1. CD001 rejects 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) and max-file-header-chars (int).
  2. CD002 rejects trailing comments longer than 60 characters by default. Excludes directives such as shebangs and linter suppressions. Option: max-length (int).
  3. CD003 rejects Python pass statements, TODO markers in comments or identifiers, and throw statements whose message or exception type marks them as unimplemented placeholders.
  4. CD004 rejects 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.
  5. CD005 flags every test file included by the active scope.
  6. CD006 rejects 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:

  1. A file passed with --config
  2. .codedecorum.toml in the repository root
  3. codedecorum.toml in the repository root
  4. Exactly one configured project manifest: [tool.codedecorum] in pyproject.toml, "codedecorum" in package.json, or one of [workspace.metadata.codedecorum] and [package.metadata.codedecorum] in Cargo.toml. Multiple configured project manifests—or both Cargo tables—produce an ambiguity error.
  5. Global configuration at $XDG_CONFIG_HOME/codedecorum/codedecorum.toml, ~/.config/codedecorum/codedecorum.toml when XDG_CONFIG_HOME is unset, or %APPDATA%/codedecorum/codedecorum.toml on Windows
  6. 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. Set enabled = false to 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 DURATION overrides the five-minute default
  • --version: print the installed version and exit
  • paths: 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:

  • LINE reports source ranges whose validity depends only on those lines. In diff-lines, only findings intersecting changed new-side lines survive. This fits forbidden calls, local syntax policies, and bounded trailing comments.
  • FILE receives 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.
  • PATH receives a root-relative PurePosixPath without 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)

Source distribution for codedecorum 0.1.0
File Size Uploaded
codedecorum-0.1.0.tar.gz 24.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for codedecorum 0.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

This release

0.1.0 This release

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