cringe-filter
cringe-filter checks a draft for the habits that make text read as if
Claude wrote it: em dashes, the it isn't X, it's Y frame, bold lead-ins,
status emoji, and replies far longer than they need to be. You tell
it where the text is going, and it tells you what to fix and how much more
often Claude does each thing than a person writing in that place.
The rules come from measurement. We compared 570k words that one
researcher (called "the writer" below) wrote in GitHub replies, issues and
reviews with 1.35M words Claude wrote in the same threads. The writer's
Discussions posts, LinkedIn posts, email, messages, papers and tutorials
cover the other contexts. The lint rules describe Claude's habits, so
they work on anyone's draft. score and prompt go further and pull a
draft toward the writer's own habits for that kind of text. While this
is hyper-specific to a single writer, the package comes with the option
to replace the default corpus with custom content (i.e., your own).
Python 3.9+. The optional rewrite
command also needs the anthropic package.
Install
pip install cringe-filter
Or run it without installing:
uvx cringe-filter contexts
Quick start
Here is a GitHub reply with four of Claude's quirks in it, saved as
reply.md:
The parser now handles empty rows — it isn't a hack, it's the real fix.
**Key changes:**
- ✅ Added a guard for empty rows
Lint it for the place it is going:
$ cringe-filter lint -c github reply.md
reply.md:1: error em-dash [10.2x, CI 7.13-16.66]: Em dash. Use a period, a colon, or commas.
parser now handles empty rows — it isn't a hack, it's the rea
reply.md:1: warn isnt-x-its-y [3.49x, CI 1.71-12.05]: The 'it isn't X, it's Y' frame. State Y and stop.
r now handles empty rows — it isn't a hack, it's the real fix. **Key changes:
reply.md:3: warn bold-run [7.22x, CI 5.29-9.98]: Bold emphasis.
t a hack, it's the real fix. **Key changes:** - ✅ Added a guard for empty r
reply.md:4: warn emoji-status [14.66x, CI 8.15-33.65]: Status emoji.
real fix. **Key changes:** - ✅ Added a guard for empty rows
4 findings, 1 errors (context: github)
[10.2x, CI 7.13-16.66] means Claude used em dashes 10.2 times as often
as the writer did in GitHub replies, with a family-wise bootstrap
interval. An error makes the command exit with code 1, so you can run it
in CI or a pre-commit hook. The warn and info findings are yours to
judge.
Written closer to how the writer replies, it passes:
I think the parser was dropping empty rows. Could you check whether a
guard there fixes it for you?
Contexts
People typically write differently in a GitHub reply vs. an academic manuscript, so every command takes a context, each with its own length budget, formatting rules and example passages:
$ cringe-filter contexts
context label docs words median budget
github GitHub reply in your own repos 7235 378991 27 110
discussion GitHub Discussions post 456 56929 51 297
third-party Issue or comment in someone else's repo 2269 136266 30 126
email Email 144 11972 57 170
message Short message (DM, chat reply, comment) 757 23572 22 65
linkedin LinkedIn post 89 9784 54 283
tutorial Tutorial or docs page 161 54974 357 1500
paper Scientific prose 206 70341 359 6000
proposal Proposal 39 13272 359 6000
any Unspecified 9960 572186 28 10000
median is the writer's median length in words (per section for papers
and proposals), and lint warns past budget. Aliases such as dm,
docs, bug-report, pr and grant work too. With --url, the
destination picks the context:
cringe-filter lint --url https://github.com/pytorch/pytorch/issues/1 draft.md
profile -c <context> prints the rate of each surface marker for the
writer and for Claude in one context.
Commands
lint
lint makes no model calls, so you can run it on every file you touch:
cringe-filter lint --context github draft.md
git diff --name-only | grep '\.md$' | xargs cringe-filter lint -c tutorial --quiet
Revisions tend to put the tells back. When agents in the lab's repos
revised paragraphs after a reviewer's comment, em dashes went from 261 to
329 and semicolons from 371 to 470. --against shows only what the new
version added:
cringe-filter lint -c paper --against draft-v1.tex draft-v2.tex
A finding tagged [preventive, no corpus support] is a pattern that never
separated the two authors in that context, and it stays at info. Where
the writer uses a pattern more than Claude does, the rule is off. That is
why lint never flags "ensure", "leverage", "comprehensive" or
"streamline".
A short phrase in double quotes is read as a mention and skipped. At
info, lint also flags a sentence longer than 90% of the writer's and
a run of seven or more words said twice.
To silence a false positive, add a comment. disable-line covers its own
line, disable-next-line the line after it, and disable-file the whole
file:
<!-- cringe-filter: disable-line em-dash -->
<!-- cringe-filter: disable-next-line bold-run, md-header -->
<!-- cringe-filter: disable-file -->
In LaTeX, write % cringe-filter: disable-line em-dash. lint reads a
.tex file as the prose in the PDF. It skips the preamble, comments,
math, tables, footnotes and the keys of \cite, \ref and \label,
treats --- as an em dash and -- as an en dash, and keeps the line
numbers. A .tex file with no context is linted as paper.
score
score adds up how far each feature pulls the draft toward Claude or
toward the writer. On the reply from the quick start:
$ cringe-filter score -c github reply.md
context: github (GitHub reply in your own repos) words: 25 sentences: 2, longest 15
style score: +11.83 log-odds, reads like Claude (a ranking of what to fix, not a calibrated probability)
length: 25 words; the writer's median here is 27, p90 110, budget 110
Burrows' Delta over the 150 most frequent words: 0.39 to the writer's register, 0.42 to Claude (closer to the writer)
feature n yours/1k writer/1k Claude/1k log-odds
(per 1000 words; structure rows per 1000 sentences or prose paragraphs)
status emoji 1 40.00 0.03 1.21 +3.60
em dashes 1 40.00 1.83 18.43 +1.90
dashes between words 1 500.00 36.20 349.60 +1.64
bold runs 1 40.00 2.93 21.47 +1.53
isn't X it's Y 1 40.00 0.03 0.12 +1.34
...
"Positive" tends to read more like Claude. The rewritten reply scores -21.12. Each row
is a log-likelihood ratio of Claude's rate against the writer's, and the
rows overlap, so use the total to decide what to fix first. It is not a
probability, and the JSON output says calibrated: false.
The Delta line takes the 150 most common words in the corpus, mostly function words, and compares how often the draft uses them with each author's average. The rows counted per 1000 sentences describe how the sentences are built, such as how often the subject is a person or a sentence opens on "The". Email and tutorials have no rows of that kind.
prompt
prompt turns the measurements into instructions any model can follow:
cringe-filter prompt --context linkedin draft.txt # the full prompt, with your draft
cringe-filter prompt --context linkedin --system-only # the filter alone
cringe-filter prompt -c github --evidence draft.md # plus the numbers behind it
cringe-filter prompt -c github --json draft.md # {system, user, evidence}
The prompt starts with two real passages that match your draft's length
and break none of the rules. Then it lists priorities for the context,
the tells to avoid and a few notes. It leaves out the rates, because
models follow numeric rules badly, and --evidence prints them for you.
One priority covers sentence structure, such as making a person the
subject. --no-structure leaves it out.
rewrite
rewrite sends the prompt to Claude and lints the result:
pip install 'cringe-filter[rewrite]'
export ANTHROPIC_API_KEY=...
cringe-filter rewrite --context email draft.txt
cringe-filter rewrite -c github --model claude-sonnet-5 --effort low draft.md
cringe-filter rewrite -c github --dry-run draft.md # print the prompt instead
cringe-filter rewrite -c paper --minimal draft.tex # edit, do not rewrite
If the result still has lint errors or measured warnings, or it dropped a
number, link, code span or path (in LaTeX, also a citation, reference or
math span), rewrite makes one more pass. It stops at two model calls,
because repeated self-revision makes good text worse. Length alone does
not trigger the second pass.
The default model is claude-opus-5 with adaptive thinking at medium
effort. A declined request is re-run on the server-side fallback model
unless you pass --no-fallback. The rewrite goes to stdout, and the pass
count, scores and remaining findings go to stderr.
Use --minimal, on rewrite or prompt, for a draft headed to review,
like a manuscript or a proposal. It asks for the smallest edit a reviewer
would make, from the draft's lint findings and a short checklist of what
the lab's reviewers asked agents to change.
audit
audit turns the lint findings into an edit spec: each sentence to fix,
with its line and the reason, and the flagged sentences to leave alone.
Give the spec and the draft to any model along with prompt --minimal:
cringe-filter audit -c paper draft.tex > spec.md
Some findings are judgment calls, because the writer also uses the
X, not Y contrast and the X is what did Y cleft, mostly to give
instructions. With --model, audit asks a model to score each one from
0 to 100 for how likely it is that Y was set up only to be knocked down.
Hits under --threshold (30 by default) move to the leave-alone list.
Without --model, nothing is dropped and you decide. The model can be
anything with an OpenAI-compatible endpoint. The default is Ollama on your
own machine, so a private draft stays private. --endpoint points it at
llama.cpp's llama-server, LM Studio or vLLM instead:
ollama pull qwen2.5:14b
cringe-filter audit -c github --model qwen2.5:14b draft.md
A frontier model separated Claude's contrasts from the writer's on this question. The 1.5B and 3B models that fit on two CPU cores did worse.
instructions
instructions writes a GitHub Copilot instruction file for each context
into .github/instructions/, or the folder you pass to --dir:
cringe-filter instructions paper proposal tutorial
cringe-filter instructions paper --apply-to "manuscript/**"
Copilot's cloud agent and code review apply each file to the paths its
applyTo globs match. By default these are .tex and .bib files for
paper, folders named for a proposal or grant for proposal, and
Markdown, RST and notebooks for tutorial. --apply-to replaces the
globs, and --exclude-agent code-review keeps code review from reading a
file. Replies are not files, so for those contexts put the output of
prompt -c <context> --system-only in .github/copilot-instructions.md
instead.
Inside a coding agent
An agent that drafts prose is already a model, so it does not need
rewrite.
SKILL.md
is a Claude Code skill that has the agent run lint and prompt and then
fix its own draft. Copy it to .claude/skills/cringe-filter/SKILL.md in
any repository where this package is installed. Clients without skills
can use the MCP server.
Where the numbers come from
The corpus and the pipeline that builds cringe_filter/data/profile.json
live in a private repository, because the corpus includes mail and direct
messages. Each rebuild opens a pull request here, so the rules follow the
data without anyone editing them. The example passages in the package
come only from public repositories and public LinkedIn posts. Mail and
messages contribute only counts.
To use your own profile, point CRINGE_FILTER_PROFILE at it. An
exemplars/ folder next to it replaces the packaged passages that have
the same file names:
export CRINGE_FILTER_PROFILE=~/private/voice/profile.json
How it was measured covers the statistics, the held-out tests and what is in the profile. To run the tests:
python -m unittest discover -s tests
Limits
- Most contexts are scored against Claude's GitHub replies, because that is the only place both authors wrote about the same work. Scoring an email that way asks whether it reads like the model or like the writer's email, which is useful, but the two are different genres.
- Papers and proposals are scored against Claude's own manuscripts and
agents' proposals. The default writer's side of
proposalis based on two proposals. - These have primarily been based on interactions with Opus 4.8, 5, 5.1, 5.5, and Fable 5, 5.1. As new models are released, behavior will vary (hopefully improving).
- Small contexts carry little signal for rare phrases. The LinkedIn corpus has 89
posts, and where a context has no verdict for a phrase,
lintfalls back to the verdict for the whole corpus. - Getting past AI detectors is not the goal. The better test: would this sound pretentious to someone who already knows what they are doing?
Metadata
Release files for cringe-filter 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cringe_filter-0.2.0.tar.gz | 162.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cringe_filter-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 324.7 kB
Release files / cringe_filter-0.2.0.tar.gz
| Download URL | cringe_filter-0.2.0.tar.gz |
|---|---|
| Size | 162.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
926d4a338ef09de0e74c3e86a21bffc7fb8134ef14f2ac3c0c5d516c182f9531
|
|
BLAKE2b-256 checksum How to use checksums |
84326916974f26e3a41967fbf4737727b962ef3271bed1e1eb42b04df817a5d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / cringe_filter-0.2.0-py3-none-any.whl
| Download URL | cringe_filter-0.2.0-py3-none-any.whl |
|---|---|
| Size | 162.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b39b25770f02907e9ed273f826e8958ec2c002bd4007241646c4b048fbd33154
|
|
BLAKE2b-256 checksum How to use checksums |
913d598c43aa1db62205b86a306680853f14ba2197d0369a4229272d52293fd4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log