grain v0.3.0

Anti-slop linter for AI-assisted codebases.

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

Install

pip install grain-lint

Usage

grain check [files...]         # check specific files
grain check --all              # check entire repo
grain check --fixNEW             # auto-fix safe rules
grain install                  # install git hooks
grain status                   # show config
grain suppress FILE:LINE RULE  # add inline suppression

What it catches

Python

RuleWhatFix
OBVIOUS_COMMENTComment restates the following lineauto
NAKED_EXCEPTBroad except with no re-raisemanual
RESTATED_DOCSTRINGDocstring just expands the function namemanual
VAGUE_TODOTODO without specific approachauto
SINGLE_IMPL_ABCABC with one concrete implementationmanual
GENERIC_VARNAMEAI filler names (process_data, handle_response)manual
TAG_COMMENTUntagged comment (opt-in)manual

Markdown

RuleWhatFix
HEDGE_WORDAI filler words in docsauto
THANKS_OPENERREADME opens with "Thanks for contributing"manual
OBVIOUS_HEADERHeader restated in following paragraphmanual
BULLET_PROSEShort bullet list that reads better as prosemanual
TABLE_OVERKILLTable with 1 row or constant columnmanual

Commit messages

RuleWhat
VAGUE_COMMITGeneric commit message (update, fix bug, wip)
AND_COMMITCommit does two things -- split it
NO_CONTEXTfix/feat with no description of what changed

Auto-fixNEW

grain check --fix

Safe rules are fixed in-place. Rules that require judgment are reported but untouched.

FIXED src/main.py:42 OBVIOUS_COMMENT -- removed comment
FIXED src/main.py:87 VAGUE_TODO -- annotated
src/main.py:103  [FAIL] NAKED_EXCEPT  broad except with no re-raise

Exit 0 = clean after fixes. Exit 1 = non-fixable violations remain.

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 (not just bare except, but handlers that catch-and-forget), docstring padding, filler words in docs, echo comments, and vague TODOs.

semgrep can match custom patterns, but requires YAML rule authoring. grain ships these rules built-in and adds .grain.toml for custom patterns without a new DSL.

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

Custom rules

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

Simple regex + file glob. No YAML, no DSL. Works with ignore, fail_on, and warn_only like built-in rules.

Suppress when intentional

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

pre-commit

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

FAQ

False positive rate?

NAKED_EXCEPT and VAGUE_TODO: near-zero. OBVIOUS_COMMENT and RESTATED_DOCSTRING: occasional coincidental overlap. Use # grain: ignore RULE_NAME for those.

Does --fix touch unsafe rules?

No. Only OBVIOUS_COMMENT (deletes line), VAGUE_TODO (annotates), and HEDGE_WORD (removes word). Everything else requires a human.

Can I write custom rules without learning semgrep?

Yes. Regex + file glob in .grain.toml. See Custom Rules above.

Python only?

For now. The architecture supports language-specific check modules. PRs welcome.