Skip to main content

prose-mint

A linter for the structural tells of AI-flavored prose. It scans markdown for em dashes, ASCII arrows, "it's not X, it's Y" and its sibling clichés, bold-colon openers used as a definition-list surrogate, AI attribution boilerplate (the "Generated with Claude Code" footer, a bare 🤖 line, Co-Authored-By trailers), and paragraphs that were hard-wrapped instead of left for the renderer to wrap.

This started inside one project (Untype) as bin/check-prose.sh plus a Claude Code skill and a CI gate. It worked, but it was trapped in that repo: using it elsewhere meant copying files and hand-editing scope. prose-mint is the extraction: one engine, several surfaces (CLI, Claude Code plugin, reusable CI action, MCP server), with a shared default ruleset that each project can tune.

What it does and does not do

It mechanically detects the structural tells above, always on. It also ships a mechanized banned-word and banned-phrase list (words like "delve", "leverage", "seamless"), but that is off by default and opt-in ([banlist] enabled = true). The reason is honesty about precision: a matcher cannot tell "navigate" the verb from the figurative tell, so the banlist is false-positive-prone. When on, it defaults to warn (reported, never fails --strict); a project can set severity = "error" to enforce. It skips inline code, blockquotes, fenced code, and a skip banlist pragma. What it still does not mechanize is the non-regex guidance against self-referential gate or CI-status narration ("prose-gate clean", "all tests green") in PR bodies; that stays a discipline for the author. The structural detectors remain the byte-for-byte port of the source scanner; the banlist is the prose-mint-only layer on top.

The detection logic is a faithful port of the Untype scanner and tracks it as the source evolves: when the source gains a category, prose-mint ports it into the shared default and re-baselines. A frozen corpus of 100+ real documents plus crafted edge cases pins the engine to the current source behavior byte-for-byte; the regression suite fails if it ever drifts.

Install

Requires Python 3.11+ (the config layer uses the standard-library tomllib, which arrived in 3.11). There are no third-party dependencies.

As a command on your PATH, from PyPI:

pipx install prose-mint               # or: uv tool install prose-mint
prose-mint scan --file path/to/doc.md

Without installing anything, straight from PyPI:

uvx prose-mint scan --file path/to/doc.md

From a source checkout, no install:

bin/prose-mint scan --file path/to/doc.md

Usage

prose-mint scan  --file doc.md              # scan one file
prose-mint scan  --stdin --label "PR #5"    # scan piped text (PR bodies, etc.)
prose-mint scan  --file doc.md --json       # structured output for tools
prose-mint scan  --file doc.md --strict     # non-zero exit on any hit
prose-mint scan  --file doc.md --no-config  # ignore any .prose-mint.toml, use defaults
prose-mint bulk  knowledge/ research/       # walk dirs, print a summary table
prose-mint bulk  --exclude '*/archive/*' .  # skip paths by glob
prose-mint unwrap --file doc.md             # join a hard-wrapped paragraph

--config <path> pins an explicit config; --no-config ignores project config entirely and runs the built-in default ruleset (useful for a canonical, reproducible scan regardless of where it runs). The two are mutually exclusive.

Text output and bulk output are byte-for-byte compatible with the original scanner, so a project migrating to prose-mint sees identical findings. --json is a new, additive contract and carries the full hit list rather than the human report's first-five truncation.

Pragmas

A document can opt out of categories with a top-of-file comment:

<!-- prose-check: skip em-dash, bold-colon-opener -->

Use skip all to silence every check. This is meant for structured-data files (trackers, schema tables) where a flagged pattern is the intended format.

Russian documents

The em dash is ordinary punctuation in Russian, not a machine-text tell. When Cyrillic exceeds 30% of the alphabetic characters, the em-dash check is skipped for that file. Every other check still applies.

Use it in every project (Claude Code plugin)

prose-mint ships in the claude-plugins marketplace. Add the marketplace once and the prose-check skill, the /prose-check command, and the MCP server are available in any project, no per-repo file copying:

/plugin marketplace add ostin-pil/claude-plugins
/plugin install prose-mint@ostin-pil-plugins

The marketplace id (ostin-pil-plugins) is the name field in the marketplace manifest. The same operations work as claude plugin marketplace add / claude plugin install on the CLI.

The skill and command resolve prose-mint from PATH first, then uvx prose-mint, so they work whether or not the CLI is installed globally. The MCP server launches via uvx --from 'prose-mint[mcp]' prose-mint-mcp, pulling the published package with no fixed checkout path required.

MCP server

A thin server exposes two read-only tools over the same engine: scan_text(text, label?) and scan_files(paths). It is credential-free by design. To lint a Notion page or Google Doc, Claude fetches the content with the MCP you already have connected and pipes the text to scan_text; this server holds no Notion or Drive auth. The plugin launches it via uvx --from 'prose-mint[mcp]' prose-mint-mcp, so fastmcp is an optional extra rather than a core dependency.

On a cold uvx cache the first connect downloads fastmcp and prose-mint, which can overrun Claude Code's MCP connect window and show "Failed to connect" until the cache is warm. Warm it once with bin/warm-mcp-cache (or the inline uvx --from 'prose-mint[mcp]' prose-mint-mcp </dev/null), then run /mcp to reconnect. The CLI and the /prose-check skill are unaffected. See issue #8.

CI gate (reusable action)

Any repo gets the gate as one stanza:

- uses: actions/checkout@v5
  with:
    fetch-depth: 0
- uses: ostin-pil/claude-plugins/prose-mint@main

The action is the prose-mint package root inside the public claude-plugins monorepo. It sets up Python, installs prose-mint from that checkout, scans the PR's changed markdown, and scans the PR title and body. What counts as in-scope is the consumer repo's .prose-mint.toml, not anything hardcoded in the action. It is warn-only by default (findings in the Actions log, no PR comment); set strict: "true" to fail the build on a hit. Inputs: strict, scan-pr-body, python-version, config. prose-mint dogfoods this action on itself via uses: ./ in .github/workflows/prose.yml.

Status

prose-mint lives in the claude-plugins monorepo and is published to PyPI as prose-mint. Done: the standalone engine and byte-for-byte regression gate, the per-project config layer, the Claude Code plugin (skill, command, marketplace), the MCP server, the reusable CI action, and the opt-in mechanized banlist. A drift guard fails the suite if the upstream Untype scanner (the source of the detection logic) gains a category prose-mint has not ported. See CHANGELOG.md for the phase log.

Metadata

Release files for prose-mint 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for prose-mint 0.1.1
File Size Uploaded
prose_mint-0.1.1.tar.gz 28.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for prose-mint 0.1.1
File Interpreter ABI Platform
prose_mint-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 49.9 kB

Release files / prose_mint-0.1.1.tar.gz

Download URL prose_mint-0.1.1.tar.gz
Size 28.1 kB
Tags Source
SHA-256 checksum
How to use checksums
6d5456a00340d426ca9d9d55ccc9882e4ad8079510bb2d066ff25d9176cfe203
BLAKE2b-256 checksum
How to use checksums
0779b2fb48c7deea4c6a127e0fee8338c590a0f5420fd37fb50676f0c2b9be9a
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 Aug 5, 2026.

Transparency log

Release files / prose_mint-0.1.1-py3-none-any.whl

Download URL prose_mint-0.1.1-py3-none-any.whl
Size 21.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f5846621744994ffc5d99502bbd2dd49b1521c56ebca887735c7dfff852a9856
BLAKE2b-256 checksum
How to use checksums
5ab06b6a6a4d32c819835fb08b3c1cd23aa719ea407fd5a2776efa6ee1fbff9e
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 Aug 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page