bluepencil
A quality gate for prose. It fails 46.9% of machine-written documents and 6.3% of human-written ones, and both of those numbers were measured rather than hoped for.
pip install bluepencil
bluepencil check draft.md
Live demo, nothing to install: huggingface.co/spaces/wolfvswhale/bluepencil
Exit code 1 when something fires at error severity, so it drops into CI, a pre-commit hook, or an agent loop without wrapping.
Why the numbers are the point
Most style linters ship a list of banned phrases and a threshold someone picked because it felt about right.
You install it, it fires on your good writing, you uninstall it.
Every threshold here was set by measuring the distribution of that statistic across 2,602 human-written documents spanning three registers: informal forum answers, formal scientific abstracts, and multi-paragraph encyclopedia articles. Each cadence gate is placed at the percentile that makes it fire on 5% of human writing. That rate is a property of the tool, not a hope, and bluepencil gates will print it back to you.
The companion repo prose-eval is the harness that produced those distributions.
What it catches
$ bluepencil check draft.md
draft.md
3:1 error throat-clearing
"In today's fast-paced world"
-> Delete it. Start with the first real sentence.
3:44 error negative-parallelism
"is not just a nice-to-have, it's"
-> State the correct thing. Leave the wrong thing out.
4:24 error editorializing
"It's important to note"
-> Just write the note.
- warn flat-cadence
Sentence lengths vary less than human writing typically does.
Measured 0.198, below the calibrated threshold of 0.237.
-> Rewrite one sentence in every three-sentence run to a
different length.
1 file checked, 0 clean, 3 errors, 1 warning
Two kinds of finding, and they need different responses.
Pattern findings point at a span with a line and column. The fix is unambiguous and takes seconds.
Cadence findings describe the document. There is no line to fix; the draft needs a pass. These are the ones that catch prose which has had every flagged word swapped out and still reads as machine-written, which is the failure mode a phrase blocklist cannot see.
Measured performance
Fire rates across 1,500 human and 1,500 machine documents, held out from calibration. "Lift" is how much more often a gate fires on machine text than human text.
| gate | human | machine | lift |
|---|---|---|---|
| editorializing | 0.1% | 18.5% | 139x |
| assistant-residue | 0.1% | 7.9% | 119x |
| hollow-conclusion | 1.1% | 21.8% | 20x |
| flat-cadence | 3.4% | 23.1% | 6.8x |
| triplet-habit | 4.7% | 17.4% | 3.7x |
| transition-crutch | 5.0% | 6.9% | 1.4x |
| repeated-openers | 5.1% | 5.9% | 1.2x |
| any finding | 31.3% | 70.1% | 2.2x |
| fails at error severity | 6.3% | 46.9% | 7.4x |
Where it doesn't work, and why
Nine of the eighteen gates fire more often on human writing than on machine writing here. All of them:
| gate | human | machine | lift |
|---|---|---|---|
| copula-avoidance | 5.8% | 0.8% | 0.1x |
| em-dash-habit | 5.5% | 0.0% | 0.0x |
| negative-parallelism | 2.4% | 0.7% | 0.3x |
| participle-tail | 1.5% | 0.1% | 0.0x |
| uniform-paragraphs | 1.5% | 0.0% | 0.0x |
| vague-attribution | 0.8% | 0.1% | 0.1x |
| inflated-significance | 0.7% | 0.2% | 0.3x |
| promotional | 0.3% | 0.2% | 0.8x |
| engagement-bait | 0.1% | 0.0% | 0.0x |
The obvious reading is that those rules are wrong.
The likelier reading is that the corpus is old.
The corpus used here, HC3, was collected around December 2022. Em dashes are the signature of current models: a pre-registered study of 69,632 medRxiv preprints (arXiv:2606.29540) puts em-dash prevalence in Discussion sections at 4.23% before ChatGPT, 8.0% in 2024, and 20.3% in 2025. HC3 predates almost all of that rise, so it cannot contain the behaviour the gate looks for. Newer paired corpora built on current models do exist, including MIRAGE, MAGA-Bench, and AITDNA, and recalibrating against one of them is the obvious next step. Until then these gates stay on and stay labelled unvalidated rather than disproven.
uniform-paragraphs is a different problem: the machine half of the corpus is single-paragraph, so the statistic is unmeasurable there and its 0.0x is an artifact of the data, not a result.
One gate shipped disabled. no-short-sentences could not be calibrated: more than 5% of human documents contain no short sentence at all, so no threshold below the target false-positive rate can ever fire. Calibration detects that and disables it rather than shipping a gate that silently never triggers.
Running it where you already are
Pre-commit. Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/wolfvswhale/bluepencil
rev: v0.1.0
hooks:
- id: bluepencil
GitHub Actions. --format github emits annotations that render inline on the pull request diff:
- run: pip install bluepencil
- run: bluepencil check . --format github --fail-on never
Drop --fail-on never when you want it to block merges.
Claude Code and other agent tools. Copy skill/SKILL.md into your skills directory. It tells the agent when to run bluepencil, how to read the two kinds of finding, and, importantly, when to overrule one.
Anything else. --format json gives you the findings with line numbers, measurements, thresholds, and suggested fixes.
Configuring it
bluepencil.toml, discovered from the file upward:
[bluepencil]
fail_on = "error" # error | warn | never
min_words = 120 # documents shorter than this skip cadence gates
[bluepencil.gates]
em-dash-habit = "off" # off | warn | error
promotional = "error"
[bluepencil.budgets]
transition-crutch = 6.0 # occurrences per 1000 words tolerated
[bluepencil.thresholds]
flat-cadence = 0.30 # override a calibrated value
Roughly one human document in twenty gets a cadence finding a good editor would overrule. That is the 5% target working as designed. Turn the gate off rather than mangling the sentence.
Recalibrating for your own writing
The shipped thresholds describe general human prose. If your house style is genuinely different, point the calibration at your own corpus:
pip install "bluepencil[calibrate]"
python scripts/calibrate.py --target-fpr 0.05
python scripts/measure.py --limit 1500
The first writes bluepencil/thresholds.json. The second regenerates the tables above.
What it will not tell you
Whether the text is true, whether the argument holds, or who wrote it. It measures the surface. A document can pass every gate and still be empty, and a good writer will sometimes trip three gates on purpose.
It is a gate, not a classifier. A document failing means it contains constructions the standard rejects, which is true of plenty of human writing, as the 6.3% says.
Sources
Rules derived from Wikipedia's Signs of AI writing, maintained by WikiProject AI Cleanup, plus a house editing standard. Calibration corpora: HC3, AI-and-Human-Generated-Text, and wikitext-103.
MIT licensed. Built by J. Alderman Lyell (@wolfvswhale).
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 bluepencil-0.1.0.tar.gz.
File metadata
- Download URL: bluepencil-0.1.0.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af7b9222bd03f0529a62f8fe68cadc486648d45134f7cb0f0b869a651a71b71e
|
|
| MD5 |
e1b6d85ff8a7a6f468bad45ea1a776b4
|
|
| BLAKE2b-256 |
fe4dbc2b62b94cadd612bbe144e6425bed7313291b2d5ce1833293a718047685
|
File details
Details for the file bluepencil-0.1.0-py3-none-any.whl.
File metadata
- Download URL: bluepencil-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e2d3e554a856a4980fe9d96ba71fad7d40263c59f38179851eb392a5d49c68c
|
|
| MD5 |
11e177074644808d2bf54e57ec3c4450
|
|
| BLAKE2b-256 |
f80600348a8aa62354d66381d5dad2e96531480b9decab1e3a6e642879d0c4fd
|