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.
pip install grain-lint
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
| Rule | What | Fix |
|---|---|---|
| OBVIOUS_COMMENT | Comment restates the following line | auto |
| NAKED_EXCEPT | Broad except with no re-raise | manual |
| RESTATED_DOCSTRING | Docstring just expands the function name | manual |
| VAGUE_TODO | TODO without specific approach | auto |
| SINGLE_IMPL_ABC | ABC with one concrete implementation | manual |
| GENERIC_VARNAME | AI filler names (process_data, handle_response) | manual |
| TAG_COMMENT | Untagged comment (opt-in) | manual |
| Rule | What | Fix |
|---|---|---|
| HEDGE_WORD | AI filler words in docs | auto |
| THANKS_OPENER | README opens with "Thanks for contributing" | manual |
| OBVIOUS_HEADER | Header restated in following paragraph | manual |
| BULLET_PROSE | Short bullet list that reads better as prose | manual |
| TABLE_OVERKILL | Table with 1 row or constant column | manual |
| Rule | What |
|---|---|
| VAGUE_COMMIT | Generic commit message (update, fix bug, wip) |
| AND_COMMIT | Commit does two things -- split it |
| NO_CONTEXT | fix/feat with no description of what changed |
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.
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.
# .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.
except Exception as e: # grain: ignore NAKED_EXCEPT
pass # intentional top-level catch
# .pre-commit-config.yaml
repos:
- repo: https://github.com/mmartoccia/grain
rev: v0.3.0
hooks:
- id: grain
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.