The Problem
AI writing has a tell. Formulaic openings, ritual conclusions, "boasts a rich tapestry," "plays a vital role" — you know it when you see it.
The obvious fix is to tell the model "sound human, drop the fluff." It doesn't stick. The tells leak right back in on the next draft, because the model is defaulting to what reads as "helpful" rather than what reads as you. You can't reliably prompt your way out of it.
The Solution
So I stopped asking and started linting. Instead of hoping the model behaves, I built a deterministic checker for the patterns that scream "an AI wrote this" — eslint, but for prose.
Two composable layers:
check_text.py(the base) — catches the universal tells: puffery, importance-emphasis, ritual conclusions, hedging, AI-vocabulary clusters, title-case headings, em-dash overuse. Works on any text.check_voice.py(a voice layer on top) — imports the base ruleset, promotes severity where you're stricter, and adds your own patterns: throat-clearing openers, label-colon fragments, a 2-em-dash-per-piece budget, thesis-opening detection.
Severity levels, --json, real exit codes. It runs before a draft ships and fixing the violations is mandatory, not a suggestion. It's CI for writing — the part most people skip.
It's public — grab the gist and point it at your own drafts.
Technical Stack
Deliberately tiny — standard library only, no dependencies:
- Python + regex — a documented rule library, each pattern tagged with a category and severity
- Base + override architecture — the voice layer
imports the base and layers on top, instead of forking one giant list. Keep shared rules in one place; layer your own voice on top. - Exit codes — non-zero on any
error-severity hit, so it drops straight into a pre-commit hook or CI
The base+override split is the engineering. It's what makes this a reusable tool instead of a personal word-blocklist.
Impact
The base catches the robot; the voice layer keeps you sounding like you. Run it on a draft and you get a line-numbered list of exactly what to cut — not a vague "make it more human."
The reusable part is the shape: a shared ruleset anyone can use, plus a thin personal layer on top. Fork the voice file, keep the base, and it's your linter. That's the bit people stop short of — they tweak a prompt when they could be running a check.
THE RECEIPTS
Base + override
check_voice.py (your voice: throat-clearing, em-dash budget, …)
│ import check_text
▼
check_text.py (universal AI tells: puffery, hedging, ritual endings, …)
▼
your draft → line-numbered violations + exit code
The base is shared and reusable. The voice layer is thin and personal. Swap in your own patterns without touching the base.