unslopify
Audit and rewrite text to remove AI-writing patterns. A deterministic Python core finds the named failures. An agent skill runs the rewrite loop until a fresh-context judge cannot tell a model touched the text. Calibrated against pre-LLM prose: 0.28 named findings per 1,000 words across Twain, Darwin, Austen, Doyle, and an RFC (numbers below).
Install
pip install unslopify
Or run it without installing:
uvx unslopify draft.md
30 seconds
$ unslopify draft.md
draft.md:1: [scene-setting] "In today's fast-paced digital world"
A weather-report opener about today's fast-moving world before the actual topic.
draft.md:1: [inflated-contrast] "is not just"
A plain statement dressed up as a reveal by denying a smaller version of itself first.
FAIL draft.md: 2 findings, 0 mechanics, 0 bank
Before:
In today's fast-paced digital world, this release is not just an
update. It is a testament to our team's unwavering commitment,
ensuring a seamless experience for every user.
After:
This release fixes the checkout crash and cuts page load from 3 s
to 1 s. Both changes came out of the June incident review.
What it checks
Four categories of named types. unslopify types prints all of them
with definitions and examples, and RULES.md is the full
catalog with a before and after pair per rule.
- formula: manufactured rhythm. Inflated contrast, negative parallelism, slogan fragments, stock triads, fake authority, canned conclusions, AI vocabulary clusters, significance inflation.
- substance: claims a reader cannot verify. Empty abstraction, tacked-on benefits, process instead of reason, unsupported claims.
- wording: buried points. Bureaucratic phrasing, hedge stacks, unexplained jargon, copula avoidance, overlong sentences, abstract noun stacks, and sentences with no concrete anchor.
- structure: packaging that delays the point. Scene-setting, request restatement, meta-announcements, redundant conclusions, over-structure.
Types are contextual signals with severities and thresholds, not a banned-word list. Mechanical checks run alongside: em and en dashes, curly quotes, sentence length, semicolon density, and 4-word phrases repeated inside one document.
CLI
unslopify DRAFT.md # audit; exit 0 pass, 1 findings
unslopify docs/ README.md # many files; directories recurse (.md .txt .rst)
cat draft.txt | unslopify # audit stdin
unslopify DRAFT.md --json # full report as JSON
unslopify DRAFT.md --brief # rewrite instructions for a model or human
unslopify DRAFT.md --fix # safe mechanical fixes to stdout
unslopify DRAFT.md --fix -w # apply the fixes in place
unslopify DRAFT.md --bank # also check the cross-document phrase bank
unslopify commit DRAFT.md --id blog-2026-08 # bank a finished document
unslopify types # print the rubric
unslopify DRAFT.md --level strict # halve soft allowances (or: relaxed)
Color respects NO_COLOR and FORCE_COLOR. Exit codes are stable:
0 pass, 1 findings in any file, 2 usage or IO error. Soft-type
thresholds and the repeated-phrase allowance scale with document
length, so long reports are judged at their own density, not a memo's.
--level (also a config key) halves or doubles the soft allowances;
hard rules are unaffected.
Suppression and config
Inline, in any file:
<!-- unslopify:disable --> skip this file
<!-- unslopify:disable=fake-authority,dash --> disable listed types or checks
some sentence <!-- unslopify:disable-line --> suppress this line
Pragmas inside code fences are documentation and do nothing. Project-wide,
in pyproject.toml:
[tool.unslopify]
disable = ["promo-tone"]
exclude = ["CHANGELOG.md", "vendored/*"]
max-sentence-words = 40
CI gate
Pre-commit:
repos:
- repo: https://github.com/youngfreezy/unslopify
rev: v0.4.0
hooks:
- id: unslopify
GitHub Actions:
- uses: youngfreezy/unslopify@v0.4.0
with:
paths: docs/ README.md
Both fail on findings, so generated slop cannot merge quietly. This repo runs its own audit in CI on every push.
The phrase bank lives at ~/.unslopify/phrase_bank.jsonl (override with
UNSLOPIFY_HOME). Committing a finished document banks its 8-word
phrases, and future drafts that reuse any of them fail the --bank check.
This is what stops a writer, or an agent, from developing stamps.
Claude Code plugin: the in-loop gate with memory
/plugin marketplace add youngfreezy/unslopify
/plugin install unslopify@unslopify
A Stop hook audits every completed response (needs
pip install unslopify on the hook's python3). On a fail it requests
exactly one clearer rewrite with the findings as the brief; the
stop_hook_active guard makes a retry loop impossible. Responses that
pass are banked, and that is the part nothing else does: when the
agent starts reusing the same 8-word phrasing across responses, the
hook quotes the repeated spans back and demands different words, not
reshuffled ones. Clean-each-time is not the bar; clean-and-not-a-stamp
is.
Where it sits
humanizer is a prompt-only skill: it rewrites by instruction, with nothing deterministic to gate on. nopus gates coding-agent responses in-loop with statistical prose measures, and does that well; it has no document mode, no named rules, and no memory. Vale and proselint lint prose against style rules but know nothing about AI-writing patterns. unslopify is the document linter and CI gate with named, sourced rules, plus the same in-loop enforcement, plus the one thing none of them have: the phrase bank, which remembers how everything before was worded and fails repetition across documents and responses. A linter tells you this text is bad. This one also tells you that you are starting to sound like yourself on autopilot.
Agent skill
SKILL.md turns any harness that supports skills into the full rewrite loop: audit, rewrite against named findings, judgment pass, uniqueness gate, fresh-context judge, final PASS. It also defines an always-on mode that applies the writing rules to every reply.
Install:
- Any agent with the skills CLI:
npx skills add youngfreezy/unslopify. - Claude Code: copy
SKILL.mdinto~/.claude/skills/unslopify/, then invoke with/unslopify. - Cursor: paste the rules from SKILL.md Mode 1 into a project or user rule, and use the CLI in the terminal for audits.
- Codex / other: include SKILL.md in the system context and expose
the
unslopifyCLI.
Using it once installed:
/unslopifyfollowed by pasted text, or by a file path, runs the rewrite pipeline on that draft and returns the clean text plus a short log of findings fixed and the judge's verdict.- "use unslopify in every reply" turns on always-on mode for the session: the agent writes under the skill's rules from then on. Put that sentence in a standing rule to make it permanent.
- The agent shells out to this package's CLI for the audit and the
phrase bank, so
pip install unslopifymakes the gates real instead of self-graded.
Library
from unslopify import audit_text, build_brief, profile_from_sample
report = audit_text(open("draft.md").read(), source="draft.md")
print(report.verdict, report.counts_by_category)
print(build_brief(report)) # instructions for the rewrite pass
voice = profile_from_sample(open("my_writing.md").read())
All models are Pydantic v2. report.model_dump_json() gives a stable,
versioned record of every audit, which is also the hook for analytics
in hosted deployments.
Pipeline
The always-on mode is simpler: a standing rule loads the skill, and the skill's writing rules apply to every outgoing message.
Design notes
- The audit is deterministic. Same text in, same report out, no model calls, no network. Judgment-only types (meaning loss, jargon the regexes miss) are the agent's job and are marked as such in the rubric.
- Edits are events, not vibes. The mechanical pass records every change as a typed RewriteEvent (line, rule id, before, after) under a StylePolicy. The skill has the agent log its own edits the same way, so a finished rewrite ships with the exact record of what moved.
- The judge must be fresh. A model that watched the rewrite approves its own choices, so the skill requires a separate context for the final read.
- Voice is preserved, not replaced. The optional
VoiceProfilemeasures the author's sentence lengths, contraction rate, and first-person rate, and the rewrite aims at those numbers.
Detection
Calibration answers what the audit flags in old human prose; detection
answers whether it separates model output from human writing at all.
scripts/detection.py runs the named-type verdict (mechanics disabled)
over fixtures/llm/, eight genuine LLM-written samples labeled as such
in each file, against the five pre-LLM human sources:
detection rate: 8/8 LLM samples flagged
false-positive rate: 0/5 human sources flagged
Add your own labeled samples to fixtures/llm/ and fixtures/human/
and rerun it on your material. The fixtures are default-assistant
register; adversarially plain model output will beat a static rubric,
which is why the agent skill adds the fresh-context judge on top.
Calibration on pre-LLM prose
The obvious question for any tool like this: what does it flag in text
written before language models existed? scripts/calibrate.py fetches
five public sources (Twain 1883, Darwin 1859, Austen 1813, Doyle 1892,
RFC 7231 from 2014), audits 5,000 words of each, and prints the table.
Run it yourself; the numbers below are from 2026-08-18.
source words named mech
Twain, Life on the Mississippi (1883) 5011 1 92
Darwin, On the Origin of Species (1859) 5003 3 206
Austen, Pride and Prejudice (1813) 5002 0 192
Doyle, Adventures of Sherlock Holmes (1892) 5001 2 289
RFC 7231, HTTP/1.1 Semantics (2014) 5004 1 235
named findings per 1,000 words: 0.28 (7 in 25,021)
The two columns are different claims and should be read differently.
Named types claim "this is a generated-text pattern", so hits on 1813 prose are false positives. There were 7 in 25,021 words. The script lists each one: "in order to" three times (Twain, Doyle, the RFC), two stacked hedges Darwin genuinely wrote, and one true artifact, Darwin's chapter-contents listing tripping the slogan-fragment shape. No pre-LLM source hit inflated contrast, fake authority, AI vocabulary, tacked-on benefits, or scene-setting.
Mechanics are style gates for modern professional writing, not AI
claims. They fire exactly where you would expect: Victorian sentence
lengths, typographic quotes in the Gutenberg files, dashes, and the
RFC's repeated boilerplate (page headers repeat as n-grams). If you
are linting literature rather than a work document, raise
--max-sentence-words and read the mechanics column as description,
not verdict.
Further reading on plain technical writing: Google developer documentation style guide.
License
MIT.
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 unslopify-0.4.0.tar.gz.
File metadata
- Download URL: unslopify-0.4.0.tar.gz
- Upload date:
- Size: 465.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff9cb466ea0fcee4ecfe035f7eb32bb88658053188b137d0f366e902e3fdc10a
|
|
| MD5 |
a8d4471dfa672f180d501ddbec602478
|
|
| BLAKE2b-256 |
39be09f000f98b870a8b7eefd49a31f997b0fa4e3badca61e15c7193641f1ecf
|
File details
Details for the file unslopify-0.4.0-py3-none-any.whl.
File metadata
- Download URL: unslopify-0.4.0-py3-none-any.whl
- Upload date:
- Size: 33.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a7e652644848d3c3e3bf248a1ea4ea99e93a2e4e8e431e5c0aad68c4ff17a0c
|
|
| MD5 |
e23e18384f66c38c27614090358ab1e2
|
|
| BLAKE2b-256 |
bb9fa307853d35f197ce17ba8df3ec500b11e3ca6e597fab52cda3f92a58f7b8
|