Skip to main content

Flag the AI tells in a piece of writing, so you can rewrite them before you ship.

Project description

noslop

The AI-writing linter. See the exact words that make your writing sound like a robot — in sixteen languages, on your own machine — and fix them before anyone else reads a word.

CI License: Prosperity 3.0.0 zero dependencies scores 0.0 on itself 16 languages try it in your browser

AI detectors hand you a verdict-shaped guess. noslop hands you the exact words to change: every buzzword, filler phrase, contrast-frame cliché, stray em dash, and flat-rhythm paragraph, underlined where it sits, with a line number and a reason. Deterministic, so the same text gets the same score every time. Local, so your unpublished draft never touches anyone's server.

The proof is the product: this README scores 0.0/1k on noslop itself, and CI fails the build the day that stops being true.

Four ways to run it: paste into the browser app, drop the CLI into a pre-commit hook or CI, wire it into reviewdog for inline PR comments, or install it as a skill so your AI coding agent checks its own prose before handing it back to you.

Try it in your browser. Paste a draft and watch the tells light up. It all runs client-side, so nothing you paste is uploaded, stored, or sent anywhere.

noslop analyzing a slop-heavy paragraph, every AI tell underlined in place

Prefer the terminal? It's also one Python file with no dependencies that drops into a pre-commit hook or CI. Same scoring engine either way, and either way it runs locally and deterministically with no network access.

Why this and not a detector

noslop AI detectors (SaaS) Vale + ai-tells
Tells you what to fix, word by word yes — line numbers and hints no — one probability score vocabulary and phrasing
Sentence-rhythm and formatting tells yes opaque no (their own docs name the gap)
Languages sixteen, each researched separately English-first English
Your draft stays on your machine always — even the browser app uploaded to their servers yes
Same text, same score, every time yes — CLI and browser parity-tested no, model-dependent yes
Pre-commit, CI, agent skill all three, zero dependencies paid API, upload required pre-commit, via a Vale install
Open source, auditable scoring every weight in one file no yes

Nothing else combines that column. And the honest limits are documented below — a clean score means these tells are absent, not that a human typed it. That's the linter's contract: catch what's catchable, show the work, leave the verdict-guessing to tools that enjoy being wrong.

As an agent skill

Point your coding agent at noslop and it'll lint its own writing before handing a PR description, commit message, or doc back to you. Two install paths, pick whichever your agent supports:

# Claude Code
/plugin marketplace add munzzyy/noslop
/plugin install noslop@noslop

# any agent using the open Agent Skills standard (Codex, Cursor, and others)
npx skills add munzzyy/noslop

Either way, the agent gets SKILL.md: what to run, how to read the score, and the rule that it flags but never rewrites - the rewrite stays the agent's job, same as it's always been yours. Ask the agent something like "check this PR description for AI tells before you post it" and it'll run noslop.py --json on the draft and act on what comes back.

In your browser

munzzyy.github.io/noslop is the whole tool as a single page. Paste or type, and every buzzword, filler phrase, construction, stray em dash, and emoji gets underlined in place, with a live score and a breakdown of exactly what tripped it. No build step, no account, no server: the page loads the same scorer the CLI uses and runs it on your machine. You can save the page and use it offline.

Nine themes from the header - Paper and Ink, plus Terminal, Sepia, Newsprint, Midnight, both Solarized variants, and a high-contrast mode. Auto follows your system by default; your pick is remembered and applied before the page paints.

the light theme, showing the score and the per-finding breakdown

Looking for a package literally named noslop on PyPI or npm? Those are different projects - an LLM-based rewriter and an old code-quality tool. This one's git-only for now; see Install.

Example

$ noslop pr.txt
words: 41   AI-tell score: 658.5/1k   -> reads as AI - needs a real rewrite

LLM buzzwords:
   1x  delves             (lines 1)
   1x  seamlessly         (lines 1)
   1x  streamline         (lines 1)
   1x  robust             (lines 2)
   1x  comprehensive      (lines 3)

Filler phrases:
   1x  "it's important to note" (lines 2)
   1x  "not just a" (lines 2)
   1x  "i hope this helps" (lines 3)

Constructions:
   1x  'not just X but Y' construction (lines 2)
        -> state it plainly instead of the contrast frame
$ echo $?
1

Sixteen languages

An LLM writing Spanish slop doesn't use translations of the English tells - it has its own crutches (sumérgete, sin fisuras, cabe destacar), and German slop leans on nahtlos and es ist wichtig zu beachten. So every language here carries its own researched lists, not a machine translation of the English ones: English, Spanish, French, German, Portuguese (Brazil), Italian, Dutch, Russian, Ukrainian, Polish, Czech, Turkish, Swedish, Romanian, Hungarian, and Finnish.

The input language is sniffed per file from stop-word coverage (standard library only, nothing phones home) and every pack keeps the same weights, so a 25+/1k verdict means the same thing in every language. Punctuation habits that differ by language are tuned per pack - Spanish dialogue dashes don't get flagged as em-dash spray. Force a language with --lang when you know better:

noslop --lang de entwurf.md
noslop informe.md            # auto-detected per file

Text the sniffer can't confidently place falls back to the English lists plus the structural checks (rhythm, formatting, emoji), and the output says so instead of pretending - --json carries language and language_source (detected / forced / fallback).

Some languages are deliberately absent rather than badly present. Danish and Norwegian Bokmål share too many function words to tell apart by this method, Greek's candidate list ran into words that are ordinary prose there, and a few others would have been guesses. Chinese, Japanese, and Korean need different tokenization entirely, so they aren't faked with the current pipeline. A pack only ships when the tells are real.

The browser app follows along: its interface reads in 32 languages (pick from the globe menu), it shows which language it detected in your text, and you can override that per paste.

Install

From PyPI (the command it installs is noslop):

pip install noslop-lint

Or with pipx, straight from the repo:

pipx install git+https://github.com/munzzyy/noslop

Or skip the install entirely, since it's a single file with no dependencies:

curl -LO https://raw.githubusercontent.com/munzzyy/noslop/main/noslop.py
python noslop.py --help

Usage

noslop draft.md                     # one file
noslop docs/*.md                    # several files
git log -1 --format=%B | noslop     # or stdin
noslop --quiet draft.md             # verdict line only
noslop --json draft.md              # results as JSON
noslop --exclude CHANGELOG.md docs/*.md   # skip a file in a glob run
noslop --lang es informe.md         # force a language pack (default: auto-detect)

The exit code is 0 when every input scores under the threshold, 1 when something scores over it, and 2 if a path couldn't be read at all - so a crash and a lint finding never look the same to a script. The default threshold is 10; change it with --threshold. docs/*.md works even on Windows shells that don't expand the glob themselves.

In markdown files, fenced code blocks and inline code are not scored, since code samples aren't prose. Pass --markdown to get the same treatment for stdin or other file extensions.

To skip files in a glob or directory run without listing them all on the command line, drop a .noslopignore next to them (one glob per line, # comments allowed) or repeat --exclude PATTERN.

Config

Editing noslop.py directly to change the word lists works, but it doesn't survive a pipx upgrade. For anything that needs to persist, drop a .noslop.json in your repo root (noslop walks up from the current directory looking for one, stopping at the first .git it finds):

{
  "ignore_words": ["robust", "leverage"],
  "ignore_phrases": ["at the end of the day"],
  "extra_words": ["synergize"],
  "extra_phrases": ["circle back"]
}

ignore_words / ignore_phrases remove entries from the built-in lists, extra_words / extra_phrases add your own on top. All four keys are optional. Use --config PATH to point at a specific file instead of relying on the directory walk, or --no-config to ignore any config file for one run.

Hooks

As a plain git hook:

# .git/hooks/commit-msg
noslop --quiet "$1" || echo "that commit message reads a bit AI"

Written like that it only warns. Drop the || echo part if you want it to actually reject the commit.

With pre-commit:

repos:
  - repo: https://github.com/munzzyy/noslop
    rev: v0.6.0
    hooks:
      - id: noslop

That runs on the markdown, text, and rst files in each commit.

As a GitHub Action, no pre-commit framework required:

- uses: munzzyy/noslop@v0.6.0
  with:
    paths: "docs/*.md README.md"

For inline PR review comments instead of a plain CI log, pipe --rdjson output into reviewdog:

python noslop.py --rdjson docs/*.md | reviewdog -f=rdjsonl -name=noslop -reporter=github-pr-review

--rdjson prints one JSON object per finding (message, file, line, severity) instead of the normal report, and pairs with --exclude/.noslopignore the same way --json does.

What it checks

The word and phrase lists live at the top of noslop.py; edit them directly if you're hacking on noslop itself, or use a config file if you just want to adjust the lists for your own project. Roughly:

  • chat-UI residue - leftover citation markup (oaicite, oai_citation, grok_card) and utm_source=chatgpt.com links. Nobody types these by hand, so one hit scores the hard verdict on its own. (Writing about these markers trips it too - quote them in code formatting, or skip the file with .noslopignore.)
  • words LLMs lean on far more than people do (delve, robust, leverage, tapestry - plus the words two 2025 word-frequency studies measured at 3x-67x their pre-LLM baseline: groundbreaking, surpassing, garnered, emphasizing, and friends)
  • boilerplate phrases (it's important to note, let's dive into, I hope this helps) and significance inflation (stands as a testament, continues to captivate, a pivotal moment)
  • the not just X, but Y contrast frame, the it isn't X, it's Y flip, and its split-sentence cousin: The problem isn't X. It's Y.
  • the dangling -ing significance closer (..., highlighting the importance of...)
  • rhetorical-question openers, mid-sentence question hooks (The result? ...), and ta-da openers (Here's why...)
  • sycophantic chat openers (Great question!) that leaked into prose
  • anaphora triads (where X, where Y, where Z) - the second one on, a single triad is just rhetoric
  • sentence-initial connective spray (Moreover... Furthermore... Additionally...), scored on density, never on one hit
  • em dashes well past normal density, emoji in prose, and emoji decorating headings
  • runs of **Term:** explanation bullets (with or without the bullet), bold-emphasis spray inside running prose
  • curly and straight quotes mixed in one document - usually a paste boundary
  • staccato runs of three-plus tiny sentences, sentence lengths with almost no variation, paragraph lengths with almost no variation
  • all of the above that's language-independent runs for every language; the vocabulary, phrase, and construction lists are researched per language, never machine-translated

Each hit has a weight, the weights are summed, and the total is scaled per 1,000 words. Under 10 usually reads fine. From 10 to 25 the text deserves a second pass, and past 25 it needs rewriting rather than word swaps. The cutoffs are judgment calls, not measurements; if they fight your material, move --threshold.

The --json field names (words, score_per_1k, verdict, language, language_source, buzzwords, phrases, patterns, and the rest) are treated as a stable interface once something depends on them - a pinned test in the suite locks the key set, so a rename shows up as a broken test rather than a silent break in whatever's parsing the output.

vs. Vale / vale-ai-tells

If you're already running Vale, vale-ai-tells covers a lot of the same ground with Vale's own style-rule format. The gap it names in its own docs is sentence-length uniformity and paragraph rhythm - it checks vocabulary and phrasing, not cadence. noslop's sentence_uniformity_cv check is exactly that: a coefficient-of-variation measure that catches the suspiciously even sentence lengths LLMs tend to produce even when the vocabulary itself passes. And noslop doesn't need a Vale install or a .vale.ini to get there - it's one file, stdlib only.

Measured, not vibes

eval/ holds a labeled corpus - 16 samples of unedited LLM output across genres, 16 samples of human writing from essays to old cookbooks to a 2016 Rails README - and a scorer that reports detection rate, false-positive rate, and AUC against it. The current engine catches 14 of 16 AI samples at the "worth a pass" threshold (up from 6 of 16 on the previous engine) and flags one human sample (Thoreau, who writes about literal landscapes with heavy em dashes - the receipts are in eval/README.md). CI runs the eval with floors, so a change that trades false positives for recall fails the build instead of shipping quietly.

Limitations

  • It matches surface patterns, not intent. A document that quotes slop in running prose gets flagged for it, quotation marks or not. Code formatting is the only escape hatch it understands.
  • The lists are one person's research-informed opinion, sixteen languages deep. If robust is a term of art in your field, edit the list or raise the threshold.
  • The sentence-uniformity check shares a mechanism with the burstiness signal that a Stanford study (Liang et al. 2023) showed flags non-native English writers far more than native ones. That's why it adds a small fixed bump instead of a verdict, why its weight didn't go up in 0.7.0, and why no rhythm check alone can push clean text past the hard threshold. If you write in a second language and noslop nags you about rhythm, that's the check to ignore.
  • A clean score doesn't mean the writing is good, and it doesn't prove a human wrote it. It means none of these particular tells showed up. A careful writer can trip it, and lazy slop can slip past it.

Contributing

Bug reports, false positives, and new buzzwords/phrases are all welcome. See CONTRIBUTING.md for dev setup and what makes a good PR.

License

Prosperity Public License 3.0.0. Free for noncommercial use: personal projects, hobby work, research, education, nonprofits, and government all qualify. Commercial use gets a thirty-day trial, and past that it needs a paid license. To sort one out, open an issue or email Munzzyy5@proton.me.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

noslop_lint-0.8.0.tar.gz (61.9 kB view details)

Uploaded Source

Built Distribution

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

noslop_lint-0.8.0-py3-none-any.whl (43.0 kB view details)

Uploaded Python 3

File details

Details for the file noslop_lint-0.8.0.tar.gz.

File metadata

  • Download URL: noslop_lint-0.8.0.tar.gz
  • Upload date:
  • Size: 61.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for noslop_lint-0.8.0.tar.gz
Algorithm Hash digest
SHA256 0db5017891472680a973b5a0eb7d3c38902660162cd2bc1af8a4c2f4ebac5cba
MD5 70526387804c71fffa09cc2aa8cdbd84
BLAKE2b-256 e6e2d917ffc02ca9f0591e44f3f51af6cb95e79af50edea8167fe458a2642746

See more details on using hashes here.

Provenance

The following attestation bundles were made for noslop_lint-0.8.0.tar.gz:

Publisher: release.yml on munzzyy/noslop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file noslop_lint-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: noslop_lint-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 43.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for noslop_lint-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 816a512b9fd8d1eeb51ace731969eba0c274f25667bd1f794de84b5fc573b2d0
MD5 bf00e77fab0ae8604ac7257c6559c398
BLAKE2b-256 9d272f8f04e197ac46b29460327bdf73ce5b13228ba9b7c18392fb1ee9896d07

See more details on using hashes here.

Provenance

The following attestation bundles were made for noslop_lint-0.8.0-py3-none-any.whl:

Publisher: release.yml on munzzyy/noslop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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