Skip to main content

unslopify

test PyPI Python License: MIT

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.

  • 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.
  • 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

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.

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.3.1
    hooks:
      - id: unslopify

GitHub Actions:

- uses: youngfreezy/unslopify@v0.3.1
  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.

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.md into ~/.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 unslopify CLI.

Using it once installed:

  • /unslopify followed 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 unslopify makes 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

pipeline

The always-on mode is simpler: a standing rule loads the skill, and the skill's writing rules apply to every outgoing message.

always-on

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 VoiceProfile measures 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

unslopify-0.3.1.tar.gz (455.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unslopify-0.3.1-py3-none-any.whl (29.7 kB view details)

Uploaded Python 3

File details

Details for the file unslopify-0.3.1.tar.gz.

File metadata

  • Download URL: unslopify-0.3.1.tar.gz
  • Upload date:
  • Size: 455.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for unslopify-0.3.1.tar.gz
Algorithm Hash digest
SHA256 8a8c7ae8f32149a52c7d74b23a0620808a6072edb736cce7d0b4996c155f8ae2
MD5 3299a72ef5672f2b238646ce942a4895
BLAKE2b-256 bce31fa878fbd376066bd15ba12a3fc9e7aa345ab9e1ebadfcad2e4ed425691f

See more details on using hashes here.

File details

Details for the file unslopify-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: unslopify-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 29.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for unslopify-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 95f07d0d657aeb09e09f93c70362e3bb899c8fefa5cab36de643e1805f51399d
MD5 c8bd6390d306ecc49a858e01f68ee016
BLAKE2b-256 2b863fb0d347e9e400da5c7eeb3854e6225ceddae54bd9fc051198dc2e9820b2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page