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 patterns before they land in version control -- and generates structured work queues so AI agents can repair them automatically.


The loop

grain check --all --json > violations.json   # scan
↓
AI agent reads violations.json               # plan
↓
agent fixes file by file, re-runs grain      # execute
↓
grain check --all --json                     # verify
↓
exit when empty                              # done

Grain is designed for both sides of this loop: it's the quality gate and the task queue. Most linters are human-facing gates. Grain is built to drive agentic remediation workflows.


Agentic workflow

--json output

grain check --all --json

Emits structured violations instead of human-readable text:

[
  {
    "file": "scripts/daemon.py",
    "line": 54,
    "rule": "NAKED_EXCEPT",
    "severity": "error",
    "message": "broad except clause with no re-raise -- swallows unexpected errors",
    "fixable": true
  }
]

Every violation includes fixable: bool. An agent can filter fixable: false (judgment calls) for human review while batch-processing the rest automatically.

grain worklog -- multi-session repair state

For large codebases (hundreds of violations), repair may span multiple agent runs or agent crashes. The worklog tracks progress across sessions so agents don't re-examine already-clean files.

grain worklog init      # snapshot current violations -> .grain-worklog.json
grain worklog status    # progress: 42/738 resolved, 696 remaining (412 auto-fixable)
grain worklog next      # print next unresolved violation as JSON
grain worklog done FILE LINE RULE  # mark a violation resolved

Agent loop using worklog:

grain worklog init
while true; do
  next=$(grain worklog next)
  [ "$next" = "null" ] && break
  # agent fixes the violation
  grain worklog done "$file" "$line" "$rule"
done
grain check --all  # final verification

The worklog survives agent restarts and can be committed alongside the codebase to coordinate multiple agents on the same repair effort.


What it detects

AI code has tells. grain flags them so humans -- or agents -- can decide whether to keep, rewrite, or suppress.

Python

Rule Severity Auto-fix 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 (opt-in strict mode)

Markdown

Rule Severity Auto-fix Description
HEDGE_WORD error AI filler words (robust, seamless, leverage...)
THANKS_OPENER error README opens with "Thanks for contributing"
OBVIOUS_HEADER warn header restated in following paragraph
BULLET_PROSE warn short bullets that read 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" -- one thing per commit
NO_CONTEXT error fix/feat with no description of what changed

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. No existing linter flags this.
  • Hedge words -- filler words in docs that signal AI-generated prose saying nothing.
  • Echo comments -- comments that restate the next line of code. AI adds these reflexively.
  • Vague TODOs -- "implement this" with no approach.

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


Quick start

pip install grain-lint   # from PyPI
pip install -e .         # from source
grain init               # scaffold .grain.toml with auto-detected excludes
grain check --all        # human-readable scan
grain check --all --json # machine-readable scan (pipe to agents)
grain check --all --fix  # auto-fix safe violations in place
grain worklog init       # start an agentic repair session
grain status             # show current config and enabled checks
grain install            # install git hooks
grain suppress FILE:LINE RULE  # add inline suppression

Config

grain init   # generates .grain.toml with sensible defaults + auto-detected excludes

Or manually:

[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/*", "reports/*"]

# Files matching these patterns are exempt from NAKED_EXCEPT (intentional in test harnesses)
test_patterns = ["test_*.py", "*_test.py", "tests/*"]

[grain.python]
generic_varnames = ["process_data", "handle_response", "get_result", "do_thing"]

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

Note: Use exclude under [grain], not a top-level [ignore] section. Grain warns on unknown top-level sections.


Custom Rules

[[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"

Suppression

except Exception as e:  # grain: ignore NAKED_EXCEPT
    pass  # intentional top-level catch
grain suppress src/main.py:42 NAKED_EXCEPT

pre-commit

repos:
  - repo: https://github.com/mmartoccia/grain
    rev: v0.3.1
    hooks:
      - id: grain

Output format

Human (default):

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

Machine (--json):

[
  {"file": "path/to/file.py", "line": 42, "rule": "OBVIOUS_COMMENT",
   "severity": "error", "message": "...", "fixable": true}
]

Exit 0 = clean. Exit 1 = errors found.


FAQ

Does grain support auto-fix? Yes. grain check --fix handles OBVIOUS_COMMENT, VAGUE_TODO, HEDGE_WORD, and NAKED_EXCEPT (minimal safe fix: narrows bare except to except Exception as e: raise). Rules requiring judgment are reported but not touched.

Can I use grain to drive an AI agent? Yes -- that's the primary agentic use case. Use --json to get machine-readable output and grain worklog to track multi-session repair progress. See the agentic workflow section above.

What's the false positive rate? Depends on the rule. NAKED_EXCEPT and VAGUE_TODO are near-zero. OBVIOUS_COMMENT occasionally flags legitimate comments where overlap is coincidental. Use # grain: ignore RULE_NAME for those.

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

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

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.1.tar.gz (34.1 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.1-py3-none-any.whl (27.4 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for grain_lint-0.3.1.tar.gz
Algorithm Hash digest
SHA256 762388377ad57256cad432957b0099ad361cef6ea6fe3c41401af4f10f1901d6
MD5 622c1a9945b8cd76d7c5a2985bc30c4b
BLAKE2b-256 01f51fd93a86e516f095380980ba125372017131578b871d311ab31cb5616421

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for grain_lint-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3efbb5ca22f935ba2c12c89f81cdea752e70158eeca7b37bff5e8bc23a4e098b
MD5 7929d107aaf0486c1380545847ae0a77
BLAKE2b-256 b724a8965983f61e70cebf9023961991e0845c6e8d473cd8eb01462d045a1311

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