I keep a writing style guide for my agents in a skill. Twenty minutes into a session it stops holding. The agent breaks a comment at column 80 in the middle of a phrase, hand-wraps a Markdown paragraph that GitHub then shows as four short lines, and leaves a ticket number in a comment that nobody outside the team can open.
The code never drifts like this, because a formatter and a linter check every edit. The comments and the docs are the part nothing checks.
subedit is the tool that does, an AI-native linter for the prose agents write. A post-edit hook checks every sentence an agent writes, autofixes the layout without a word, and hands back to the model only what needs rewriting, with the fix stated.
AI-Native by Design
Ruff and ESLint were built for a person reading a terminal, who remembers the rules, runs the checker, and fixes a finding from a short label. A model does none of these reliably, so subedit is built the other way round.
- Feedback It Cannot Skip: an agent does not call a checker for a rule it has forgotten, so subedit is a post-edit hook that runs on every file the agent writes, in Claude Code or Codex. The check happens whether or not the agent remembers the rule.
- Silent Autofix: a fix that changes a few characters or where a line breaks is applied without a message. Every message costs the model attention, and fifty layout complaints would crowd out the work it was doing.
- Rewrites as Instructions: only a finding that needs the sentence's meaning goes back, and it names the rule, measures the problem, and states the fix. The hook blocks the edit only for these, and passes a judgment call such as a date in a comment as context instead.
- Judgment Shipped Along: the Claude Code plugin ships a skill with the criteria for each finding no tool can fix, such as when a spec identifier stays and a ticket number goes.
Compared With Ruff and Friends
The tools a repository already runs treat a comment as a line of code. subedit treats it as a sentence.
| Tool | What It Does With Prose | What It Leaves |
|---|---|---|
| ruff format, Black, Prettier | Keeps comment text exactly as written | A comment broken mid clause stays broken |
| ruff, ESLint | Flags a line past the width, or a missing docstring | No fix, and no idea where the sentence may break |
| Vale | Checks wording against a style | Measures the extracted text rather than the source line, and fixes nothing |
subedit runs alongside them. It reads the width from the ruff, Black, Prettier, or Biome config the project already has, and bundles a Vale style and AutoCorrect for word-level and CJK spacing rules, so all three engines share one report.
Examples
Each example below is real output from subedit.
A Comment
An editor wraps a comment at whatever column it reaches.
def reflow(comment: Comment, width: int) -> Comment:
# A sentence that fits the width stays on one line of the comment, and a longer
# one breaks only at a comma that ends a clause.
return comment.reflow(width)
subedit fixes it and says nothing. The sentence is too long for one line, so it breaks at the comma where its first clause ends.
def reflow(comment: Comment, width: int) -> Comment:
# A sentence that fits the width stays on one line of the comment,
# and a longer one breaks only at a comma that ends a clause.
return comment.reflow(width)
A Markdown Note
An agent writes a note the way it writes a comment, hard-wrapped, with a bold heading and lowercase labels.
## **how the cache works**
The cache keeps one entry per file (keyed by its path), and drops the entry
when `mtime` changes, so a stale read never reaches `load_config()`.
- **eviction policy**: the oldest entry goes first once the cache holds
more than `max_entries` items.
```python
def load(path: Path) -> Entry:
# Read the entry from disk when the cache misses it, and store the result for
# the next caller.
return _store.get(path) or _read(path)
```
One pass fixes all of it without a message. Each paragraph and list item takes one line, the heading loses its bold and gains Title Case, and the label is cased. Inline code such as load_config() is left exactly as written. With code-blocks = true, the comment inside the fenced block is checked as Python and joined too.
## How the Cache Works
The cache keeps one entry per file (keyed by its path), and drops the entry when `mtime` changes, so a stale read never reaches `load_config()`.
- **Eviction Policy**: the oldest entry goes first once the cache holds more than `max_entries` items.
```python
def load(path: Path) -> Entry:
# Read the entry from disk when the cache misses it, and store the result for the next caller.
return _store.get(path) or _read(path)
```
The heading still reads as a question, and only the author can rename it, so the hook passes that on as a warning the agent may act on without being blocked.
A Rewrite by the Agent
Some findings need the sentence's meaning, so no tool can fix them. The agent that wrote the sentence can, as long as something tells it what is wrong and how to fix it. Here a public function carries a plain comment, which never shows on hover, and the comment joins two clauses with a semicolon.
# Retry a request with backoff; gives up after five attempts.
def retry(request: Request) -> Response:
return _send_with_backoff(request, attempts=5)
The hook blocks the edit and hands the agent this.
2 findings need rewriting in retry.py.
Each states the fix. Apply it in place, do not add a suppression comment.
line 1 [plain-comment-on-public]
retry is public but documented with a plain comment. Move the text into a """ doc comment so tooling renders it on hover.
line 1 [subedit.Semicolon]
Semicolon found. Split into two sentences or use a comma.
The agent rewrites the comment in its next step, and the check passes. Nobody reminded it of the style guide, and nobody had to review the comment for it.
def retry(request: Request) -> Response:
"""Retry a request with backoff, and give up after five attempts."""
return _send_with_backoff(request, attempts=5)
The House Style
Tree-sitter finds the prose in Python, TypeScript, JavaScript, YAML, JSON, and Markdown, so code and commented-out code are never touched. Every rule can be set to error, warning, or off by name.
- Layout: a comment breaks only at a clause comma, and a Markdown paragraph takes one source line.
- Placement: a public symbol gets a docstring or JSDoc, with no banner and no restated signature.
- Wording: no change narration, ticket numbers, or dates in comments, and no mid-sentence colon, semicolon, or dash.
- Headings: Title Case noun phrases, not bold, each opened by a lead sentence.
Getting Started
The Claude Code plugin needs only uv, and acts in any project with a subedit.toml, which may be empty.
/plugin marketplace add mroops0111/subedit
/plugin install subedit@mroops
For Codex or CI, install the CLI. The README covers the Codex hook.
uv tool install 'subedit[vale]'
subedit fix src/ --write # apply the autofixes
subedit check src/ # report what still needs rewriting
Closing
This post went through subedit before it went up. If it gets a sentence wrong in your codebase, or your house style needs a rule it lacks, open an issue with the sentence.
- Repository: github.com/mroops0111/subedit

Top comments (0)