A console prose linter — readability checks plus opt-in AI-writing / de-slop checks. Like flake8, but for English.
Project description
nabokov
A console linter for English prose: readability, and the tells of AI writing.
One warning per line, toggled like flake8.
Code gets review. Prose gets a shrug. nabokov catches hard sentences, adverbs,
passive voice, wordy phrases, and qualifiers. Every report carries a readability
grade. Opt-in checks catch the tells of AI writing: puffery like delve, chatbot
filler, em-dash pileups, flat robotic rhythm. Findings print as warnings you pipe
into an editor or CI. Each check is a rule with its own code, so you toggle them
like flake8.
Try it
uvx nabokov draft.md # one command, no setup; fetches the model on first run
Point it at a paragraph of AI slop and it answers:
$ nabokov --ai release-notes.md
release-notes.md:1:14: NB502 AI tell: puffery 'leverages'
release-notes.md:1:51: NB502 AI tell: puffery 'transformative'
release-notes.md:1:75: NB501 AI tell: negation-contrast 'It's not just an update, it's'
release-notes.md:1:107: NB502 AI tell: puffery 'paradigm'
release-notes.md:1:127: NB302 passive voice: 'was celebrated by the whole team'
5 issues in 1 file
Or try it in your browser → (no install).
Why
Code has linters, but prose rarely does. Style guides live in people's heads. nabokov moves them into your terminal. It points at the sentence that reads hard and says why.
Install
Requires Python 3.12 or newer.
uv tool install nabokov # or: pipx install nabokov
nabokov download-model # one-time: fetch the spaCy model (en_core_web_sm)
nabokov draft.md
That spaCy model is why nabokov is accurate. It reads grammar, not patterns: it
tells a verb from a noun and follows sentence structure, so it raises far fewer
false alarms than a regex linter. nabokov fetches it on the first run, and
nabokov download-model does it up front. For local development, uv sync pulls
everything.
Usage
nabokov draft.md # colored report for humans
nabokov --format=flake8 x.md # path:line:col: CODE message
cat notes.txt | nabokov - # read from stdin
nabokov docs/ # walk a directory of .txt / .md / .html files
nabokov --max-grade 9 x.md # exit non-zero if the grade goes over 9
nabokov --target essay draft.md # judge against the ESSAY reading level
nabokov --select NB302 x.md # run one rule
nabokov --ignore NB301 x.md # skip a rule
nabokov --stats x.md # document metrics: grade, sentence length, burstiness
nabokov --list-rules # print every code
--stats prints one metrics line per file (also in --format json as summary).
Burstiness is the sentence-length coefficient of variation. High means varied,
human rhythm. Low means flat and machine-uniform. Diff it between two drafts to
catch a rewrite that got polished flat.
nabokov reads plain text, Markdown, and HTML. For .md and .html it blanks the
markup: code, tags, and link URLs. It then checks only the visible prose, so findings
point at real writing. For stdin, --stdin-display-name draft.md sets the type.
Exit codes follow flake8: 0 when clean, 1 on findings, 2 on a usage
error.
Output formats
Choose one with --format:
- Color (
--format=color) highlights snippets and adds a grade summary. It is the terminal default. - Flake8 (
--format=flake8) prints one finding per line, for editors and CI. - JSON (
--format=json) returns diagnostics plus the document grade. - GitHub (
--format=github) emits workflow annotations for GitHub Actions.
Rules
Run nabokov --list-rules to see them all. The full reference lives in
docs/RULES.md.
| Code | What it flags |
|---|---|
NB201 / NB202 |
The very hard and hard reading levels. |
NB203 |
A main clause buried after 20+ words of build-up (advisory). |
NB301 |
Adverbs. |
NB302 |
Passive voice. |
NB303 |
Qualifiers and hedges. |
NB304 |
Nominalizations behind light verbs: "came to an agreement" → agreed. |
NB305 |
Dummy subjects: "There are many resorts in Colorado" → "Colorado has…". |
NB306 |
Repeated words: "Paris in the the spring". |
NB307 |
Uncomparables: "very unique", "most perfect". |
NB401 |
Wordy phrases, with a simpler suggestion. |
NB601 |
Abstract, "empty prose" paragraphs, scored against the Brysbaert concreteness norms (advisory). |
NB101 |
The document grade, reported with --max-grade. |
Reading-level targets
One bar does not fit every text. --target sets the level nabokov holds a sentence
to (case-insensitive):
accessible: plain language; sentences count ashardfrom grade 8,very hardfrom 12.normal: the default;hardfrom grade 10,very hardfrom 14.technical: docs for expert readers;hardfrom grade 14,very hardfrom 18.essay: essays, blog posts, opinion pieces. The TECHNICAL thresholds, plus the loosest style budgets for a writer's voice.social: short-form posts. Plain-language thresholds. Staccato fragments and repeated openers are the genre's voice, not AI tells.email: business email. A high-trust audience, so the tightest style budgets of any target.
nabokov --target technical api-guide.md
nabokov --target essay draft.md
Each target also carries style budgets, counted per 1000 words. Adverbs, passive
voice, qualifiers, and wordy phrases stay info within budget. Over budget, they
become warnings. To make a target stick, set target in your
config instead of passing the flag each run (see below).
Signs of AI writing (opt-in)
nabokov also spots common LLM tells (NB5xx). The lists come from the Wikipedia guide
Signs of AI writing
and community threads. It catches the it's not X, it's Y construction and puffery like
delve or tapestry. Promotional phrases, chatbot filler like Great question!, and
overused transitions all trip it. It also flags em-dash and emoji overuse. Rule-of-three
fragments, flat sentence rhythm, and repeated openers round it out. It even puts a
number on the flat rhythm: the burstiness metric from --stats (see Usage).
These checks stay off by default, because they often flag a writer's own voice. Turn them on with a flag:
nabokov --ai draft.md # the core checks plus the AI-writing checks
nabokov --ai-only essay.md # only the AI-writing checks
--ai is shorthand for --extend-select NB5, and --ai-only for --select NB5.
Pair it with the agent skills
The linter catches the mechanical part. Two sibling skills teach an agent to act on it.
nabokov-editor fixes the findings, then reads for what rules miss: empty
sentences, invented detail, hollow closers. Fixes keep your meaning. Big edits wait
for your approval.
nabokov-copywriter does the opposite move. A clean draft can still be flat, so
this skill adds. You pick a goal (sell, reach, provoke, or build trust), and it
rebuilds the draft toward it: rhythm, a concrete scene, a proven structure, a call to
action. It works from your real facts and asks when a scene needs a detail it doesn't
have, then re-lints so the polish never slides back into slop.
# Claude Code
/plugin marketplace add viewflow/nabokov
/plugin install nabokov@viewflow
# Cursor, Codex, Gemini CLI, and other agents (via the skills CLI)
npx skills add viewflow/nabokov
Then ask your agent to de-slop a file, or to make copy land. Skill details live in skills/nabokov-editor/SKILL.md and skills/nabokov-copywriter/SKILL.md.
The Telegram bot
The linter also lives in Telegram: @nabokov_editor_bot.
Behind it is a DeepSeek-powered editor that keeps your voice. It cuts the slop,
restores the rhythm, and asks when a fact is missing. The reply carries the
AI-likeness score before and after. Editor and copywriter modes; first three
texts free. The code lives in bot/.
Configuration
Put settings under [tool.nabokov] in pyproject.toml, or in a .nabokov.toml.
nabokov walks up from the current directory to find one. CLI flags win.
[tool.nabokov]
target = "NORMAL" # ACCESSIBLE | NORMAL | TECHNICAL | ESSAY | SOCIAL | EMAIL
ignore = ["NB301"] # e.g. stop flagging adverbs
[tool.nabokov.budgets] # optional: per-1000-word style budgets (see docs/RULES.md)
NB301 = 20 # adverbs stay advisory (info) up to this density
Suppress one line inline:
This sentence is fine. <!-- nabokov: ignore NB302 -->
How it works
nabokov scores readability with the Automated Readability Index (ARI). Word characters drive the grade, so nabokov counts no syllables.
The adverb list and the phrase dictionary began as classic lists for plain language. We added extra hedges and more phrase alternatives. A fuller set of irregular participles feeds the passive check.
spaCy handles the parsing. Passive voice reads the auxpass dependency. Adverbs read
the part-of-speech tag plus the -ly suffix. The pipeline loads once and runs on every
file.
Development
uv run pytest # the test suite
uv run ruff check . # lint
uv run ruff format . # format
uv run pyright # type-check
The nabokov-editor skill drives the linter inside an agent loop.
License
MIT.
Credits
Inspired by the Hemingway Editor. Parsing uses spaCy. The name is a nod to a writer who cared about sentences.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nabokov-26.7.6.tar.gz.
File metadata
- Download URL: nabokov-26.7.6.tar.gz
- Upload date:
- Size: 498.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94760690cf77acfd23882d30f74a8556d86ef8a120f32ec54ff0cd88b4851598
|
|
| MD5 |
d9ba1167bdc9e4feb3a5c15e935c7fd2
|
|
| BLAKE2b-256 |
7b970401250721984fc6eae7ef85c99967ddb473b12ccef941cc68fb1e5d8d1d
|
File details
Details for the file nabokov-26.7.6-py3-none-any.whl.
File metadata
- Download URL: nabokov-26.7.6-py3-none-any.whl
- Upload date:
- Size: 298.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ceb2438d354d645db46782b3b67bd3a4ab8ad681a51bad88f370b42e6b6c25f4
|
|
| MD5 |
b74fc1d72f40b41cad3666669f8e9cef
|
|
| BLAKE2b-256 |
263ef1550f0a2403cb9b2ddab22cd3d7a03a89b5ab6550ae7dbf30952eada799
|