Skip to main content

Anti-slop linter for AI-assisted codebases

Project description

grain

Anti-slop linter for AI-assisted codebases. Detects AI-generated code and documentation patterns before they land in version control.

What it does

AI code has tells. grain flags them so a human can decide whether to keep, rewrite, or suppress.

Why not ruff / pylint / semgrep?

Those tools check syntax, style, types, and known bug patterns. They're essential. grain doesn't replace them.

grain catches behavioral patterns specific to AI code generation that traditional linters miss:

  • Silent exception swallowing -- AI wraps everything in try/except with no re-raise. ruff has E722 for bare except, but doesn't check whether the handler re-raises or just logs and moves on.
  • Docstring padding -- AI restates the function name as a sentence and calls it documentation. No existing linter flags this.
  • Hedge words -- "robust", "seamless", "comprehensive" in docs signal AI-generated prose that says nothing. No linter checks for this.
  • Echo comments -- comments that restate the next line of code. AI adds these reflexively. Existing linters check comment style, not content.
  • Vague TODOs -- "implement this" with no approach. Traditional linters flag missing TODOs, not empty ones.

semgrep can do custom pattern matching, but requires writing YAML rules per pattern. grain ships with these rules built-in and adds .grain.toml for custom patterns without learning a new DSL.

Run grain alongside ruff/pylint. They solve different problems.

Quick start

pip install grain-lint   # from PyPI
pip install -e .         # from source

Usage

grain check [files...]      # check specific files
grain check --all           # check entire repo
grain check --fix           # auto-fix safe violations in place
grain install               # install git hooks into .git/hooks/
grain status                # show current config and enabled checks
grain suppress FILE:LINE RULE  # add inline suppression comment

Checks

Python

Rule Severity Description
OBVIOUS_COMMENT error comment restates the following line
NAKED_EXCEPT error broad except clause with no re-raise
RESTATED_DOCSTRING warn docstring just expands the function/class name
VAGUE_TODO error TODO without specific approach or reason
SINGLE_IMPL_ABC warn ABC with exactly one concrete implementation
GENERIC_VARNAME error function named with AI filler (process_data, etc.)
TAG_COMMENT warn untagged comment -- requires # TAG: description format (opt-in)

Markdown

Rule Severity Description
HEDGE_WORD error AI filler words -- see hedge_words in config
THANKS_OPENER error README/CONTRIBUTING opens with "Thanks for contributing"
OBVIOUS_HEADER warn header content fully restated in following paragraph
BULLET_PROSE warn short bullet list that reads better as a sentence
TABLE_OVERKILL warn table with 1 row or constant column

Commit messages

Rule Severity Description
VAGUE_COMMIT error subject too generic (update, fix bug, wip...)
AND_COMMIT error subject contains "and" -- do one thing per commit
NO_CONTEXT error fix/feat with no description of what changed

Config

Create .grain.toml in your repo root:

[grain]
fail_on = ["OBVIOUS_COMMENT", "NAKED_EXCEPT", "HEDGE_WORD", "VAGUE_TODO", "VAGUE_COMMIT"]
warn_only = ["RESTATED_DOCSTRING", "SINGLE_IMPL_ABC", "BULLET_PROSE"]
ignore = []
exclude = ["tests/*", "migrations/*"]

[grain.python]
generic_varnames = ["process_data", "handle_response", "get_result", "do_thing"]
# allowed_comment_tags = ["TODO", "BUG", "FIX", "PERF", "NOTE", "HACK", "FIXME", "XXX", "SAFETY", "REVIEW"]

[grain.markdown]
hedge_words = ["robust", "seamless", "leverage", "cutting-edge", "powerful",
               "you might want to", "consider using", "it's worth noting", "note that"]

Custom Rules

Define your own pattern-matching rules in .grain.toml. Each custom rule has a name, a regex pattern, a file glob, a message, and an optional severity. grain evaluates them alongside built-in rules.

[[grain.custom_rules]]
name = "CONST_SETTING"
pattern = '^\s*[A-Z_]{2,}\s*=\s*\d+'
files = "*.py"
message = "top-level constant assignment -- use config or env vars"
severity = "warn"

[[grain.custom_rules]]
name = "PRINT_DEBUG"
pattern = '^\s*print\s*\('
files = "*.py"
message = "print() call -- use logging instead"
severity = "error"

[[grain.custom_rules]]
name = "FIXME_DEADLINE"
pattern = 'FIXME(?!.*\d{4}-\d{2}-\d{2})'
files = "*.py"
message = "FIXME without a deadline date (YYYY-MM-DD)"
severity = "warn"

Fields:

Field Required Description
name yes Uppercase + underscores (e.g. MY_RULE)
pattern yes Python regex, matched per-line
files yes File glob (e.g. *.py, *.md)
message yes Human-readable violation message
severity no "warn" (default) or "error"

Custom rule names work with ignore, fail_on, and warn_only just like built-in rules. Invalid rules (bad regex, missing fields) are skipped with a warning.

Opt-in rules

Some rules are strict enough that they're off by default. Add them to warn_only or fail_on in .grain.toml to activate:

[grain]
warn_only = ["TAG_COMMENT"]

TAG_COMMENT requires every comment to use a structured tag format (# TODO: ..., # NOTE: ..., etc.). Section headers, dividers, shebangs, type: ignore, and noqa are automatically skipped.

Suppression

Add # grain: ignore RULE_NAME to the offending line:

except Exception as e:  # grain: ignore NAKED_EXCEPT
    pass  # intentional -- this is a top-level catch

Or use the CLI:

grain suppress src/main.py:42 NAKED_EXCEPT

pre-commit framework

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/mmartoccia/grain
    rev: v0.3.0
    hooks:
      - id: grain

FAQ

What's the false positive rate? Depends on the rule. NAKED_EXCEPT and VAGUE_TODO have near-zero false positives. OBVIOUS_COMMENT and RESTATED_DOCSTRING occasionally flag legitimate comments where overlap is coincidental. Use # grain: ignore RULE_NAME for those cases, or adjust thresholds in the source.

Does grain support auto-fix? Yes. grain check --fix auto-fixes safe rules (OBVIOUS_COMMENT removal, VAGUE_TODO annotation). Rules requiring judgment (NAKED_EXCEPT, RESTATED_DOCSTRING) are reported but not auto-fixed.

Can I write custom rules without learning semgrep YAML? Yes. Custom rules use simple regex + file glob in .grain.toml. See the Custom Rules section above.

Does grain work with pre-commit? Yes. See the pre-commit section. The --fix flag is not recommended in pre-commit hooks (fixes should be reviewed, not auto-applied in CI).

Python only? For now. The architecture supports adding language-specific check modules. PRs welcome.

Output format

path/to/file.py:42  [FAIL] OBVIOUS_COMMENT  "# return result" restates the following line
path/to/README.md:7  [FAIL] HEDGE_WORD  "robust" signals AI-generated prose

Exit 0 = clean. Exit 1 = errors found (pre-commit blocks the commit).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

grain_lint-0.3.0.tar.gz (30.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

grain_lint-0.3.0-py3-none-any.whl (23.9 kB view details)

Uploaded Python 3

File details

Details for the file grain_lint-0.3.0.tar.gz.

File metadata

  • Download URL: grain_lint-0.3.0.tar.gz
  • Upload date:
  • Size: 30.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for grain_lint-0.3.0.tar.gz
Algorithm Hash digest
SHA256 bed1b5b1bf665a93ce9fa82e15cc03f2c6dbaa42e5d604c1e801644f29c38bf7
MD5 be2eab63802b97e1e86e1a12f02c4349
BLAKE2b-256 01cb8e2693867144772646005d751d3a9bd065072fc3ca7fb93364ff0819aa30

See more details on using hashes here.

File details

Details for the file grain_lint-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: grain_lint-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 23.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for grain_lint-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1810007016d9b30491ed4177db05edb77a9305c7f5d62c61944d2d68340c1bf5
MD5 f1327e5a3b8aa9f24f2d6284b7a59468
BLAKE2b-256 ca93d789a56e05d4bc43e71cc499835d614708dc58ea223f01d3faa1dcd083a2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page