Pre-emit AI-vernacular self-check an agent runs on its OWN prose
Scanned 9/1/2026
Install to Claude Code
npx -y skills add rjmurillo/ai-agents --skill prose-self-check --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Prose Self Check?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/rjmurillo-prose-self-check-ai-agents)More formats (shields.io, HTML) on the badges page.
---
name: prose-self-check
version: 0.3.0
description: Pre-emit AI-vernacular self-check an agent runs on its OWN prose
before writing a session-log narrative, ADR context section, retrospective, or
PR description. Four layers ordered by reader-trust, not ease of detection.
Use when you say `prose self-check`, `audit my writing for AI tells`, `does
this read as AI-written`, or before emitting any prose artifact. Do NOT use
for code style (use style-enforcement) or to rewrite human-authored text.
license: MIT
---
# Prose Self-Check
Run this on prose you are about to emit. It catches AI-vernacular tells before
the artifact lands. It audits your OWN output, not human-authored text. It does
not touch code style (that is `style-enforcement`).
The layers are ordered by reader-trust, not by ease of detection. The cheap
keyword signal (Layer 1) and the real reader-cited signal (Layers 2-4) point in
different directions. Weight structural and semantic findings above lexical.
Empirical ranking from a public 89k-post study (John Carter, "I pulled ~90,000
Reddit posts about what makes writing sound like AI (Part 2)," r/ClaudeCode,
2026-06-22): em-dash 7.1% of reader cites, flat rhythm 4.0%, "not X, it's Y"
2.8%. Top keyword matches (`however`, `moreover`, `nuanced`) are ~0% reader-cited.
## Triggers
- `prose self-check`
- `audit my writing for AI tells`
- `does this read as AI-written`
- `check this prose before I send it`
## When To Use
Run before emitting any prose artifact: session-log narrative, ADR context
section, retrospective, PR body, issue body, design doc, agent-authored comment.
Skip for: code (use `style-enforcement`), pure data/config files, human-authored
text you were asked to preserve, and one-line acknowledgements with no prose.
## Process
Layers 1 to 3 are pattern matches over text, so scripts run them. Layer 4 is
the one no scanner can do, and it is where your attention belongs.
Start by running both helpers over the artifact:
```bash
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/prose_lint.py" FILE
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/burstiness.py" FILE
```
`prose_lint.py` covers Layers 1 and 2 and exits 1 on any high-severity
finding. Fix what it reports, re-run until it exits 0, then do Layer 4 by
hand. Do not scan for these tells by eye; you will miss some and spend
attention you need for Layer 4.
### Layer 1: Lexical (weakest signal)
`prose_lint.py` runs this layer. It reports two tiers, and the tiering is the
point:
- **High-signal (`high`, fails the run).** Em-dash (U+2014) and en-dash
(U+2013), model-identity phrases (`as an AI language model`), and the
strongest tells in the banned list. Remove every one.
- **Low-signal (`info`, never fails).** Words that top keyword scans but
readers rarely cite. The script reports them so Layer 4 can adjudicate:
a flagged word inside a paragraph that makes a real claim stays; one
inside filler goes with the filler. A blanket scrub here produces the
"robot pretending not to be a robot" over-correction and reads worse. The
exact low-signal set is the `LOW_SIGNAL_WORDS` constant in the script.
The canonical word list is NOT duplicated in this skill or in the script. It
lives in one place, the "Banned Vocabulary" section of `.claude/rules/voice.md`,
and `prose_lint.py` parses it from there at runtime. A forked list drifts.
The dash ban is also a hard repo rule (`.claude/rules/universal.md` MUST NOT
5). The script verifies it at the character level, so no eyeballing is needed.
### Layer 2: Structural
Sentence- and paragraph-shape tells. These are the #1 reader-cited
sentence-level signals and survive any keyword pass. `prose_lint.py` detects
the three that have a fixed shape:
- **Contrast framing** (`contrast_framing`): `not X, it's Y` / `it's not just
X, it's Y` / `X isn't about Y, it's about Z`. The single most-cited sentence
tell. Rewrite to state the claim directly.
- **Manufactured trailing offers** (`trailing_offer`): a sentence proposing
new, uninvited scope (`Want me to also`, `I could also`, `Let me know if
you'd like`). Delete it. (Mirrors the STOP-TOKEN rule in CLAUDE.md.)
- **Signposting / throat-clearing openers** (`signposting`): `Honestly,` /
`Look,` / `Let's dive in` / `It's worth noting that` / `In today's
landscape`. Delete the opener; lead with the point.
Two shapes stay yours because judging them needs the meaning, not the string:
- **Rule-of-three padding**: three parallel adjectives or clauses where one
carries the meaning ("fast, reliable, and scalable"). Cut to the
load-bearing term. Whether the third term is padding or content is a
reading, not a match.
- **Inline-header lists**: bullets that each open with a bolded restatement
("**Speed**: it is fast.") when the bold adds nothing. Drop the label or
fold into prose. The same shape is correct when the label is a real index
into a set, which is why the script does not flag it.
### Layer 3: Distributional (proxy only)
Two reader-cited tells invisible to keyword passes. Use the proxies below; a
classifier (Pangram, GPTZero) is OUT of scope because it cannot run in-agent.
- **Burstiness proxy (flat rhythm, #2 reader cite)**: AI prose clusters near a
uniform sentence length. Human prose varies. Compute the spread of sentence
lengths in the artifact; if they are all within a narrow band, break some up
and run others together. Use the helper:
```bash
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/burstiness.py" FILE
```
It prints sentence count, mean length, standard deviation, and a flat-rhythm
warning when variance is low. The warning is a prompt to vary rhythm, not a
hard gate.
- **Concreteness proxy**: count named entities, numbers, and file paths. Prose
with near-zero of these is usually saying nothing (feeds Layer 4). The same
helper reports a concreteness count.
### Layer 4: Semantic (emptiness gate)
The top-5 tell no scanner sees: fluent text that says nothing. For each
paragraph, name the one disagreeable claim it makes, the thing a reasonable
reader could push back on. If you cannot name it, the paragraph is filler.
Either give it a real claim with evidence or cut it.
This is where low-signal Layer 1 words get adjudicated: a `comprehensive`
inside a paragraph that survives the emptiness gate stays; one inside filler
goes with the filler. The tiers are intersected with the voice rule, so a
low-signal word the rule does not ban (`however`, `thus` today) stays dormant
and never reaches you.
## Output
After running all four layers, the artifact should:
- Exit 0 from `prose_lint.py`, which means zero dashes, no high-signal
lexical tells, no contrast framing, no manufactured trailing offer, and no
signposting opener.
- Have every `info` finding adjudicated by Layer 4 rather than scrubbed.
- Vary sentence length (no flat-rhythm warning, or a deliberate reason to keep it).
- Have every paragraph make a nameable, disagreeable claim.
Report what you changed, layer by layer, so the next reader can audit the pass.
## Anti-Patterns
- Scanning for these tells by eye. `prose_lint.py` does Layers 1 and 2
exactly; hand-scanning is slower, misses matches, and spends the attention
Layer 4 needs.
- Stopping when `prose_lint.py` exits 0. It clears Layers 1 and 2 only. A
clean exit says nothing about whether the prose says anything.
- Reflexively scrubbing every `info` finding. Over-correction reads as AI
overcompensating. Cut a low-signal word only when Layer 4 also fails.
- Copying the banned-word list into this skill or into the script. The list
lives in `.claude/rules/voice.md`; the script parses it from there.
- Running this on human-authored text. This is self-check on agent output only.
- Treating the burstiness warning as a hard gate. It is a proxy and a prompt.
## Scripts
### prose_lint.py
Layers 1 and 2. Reports dashes, banned vocabulary (parsed from the voice
rule), contrast framing, trailing offers, signposting openers, and
model-identity phrases. Fenced code blocks and inline code spans are skipped.
Usage:
```bash
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/prose_lint.py" FILE [FILE ...]
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/prose_lint.py" - < draft.md
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/prose_lint.py" FILE --json
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/prose_lint.py" FILE --rules PATH
```
Each finding prints as `FILE:LINE:COLUMN: SEVERITY: KIND: MATCH (note)`.
`-` reads stdin, which is how you check a draft that is not a file yet.
Every run ends with what it examined, not only what it found: `0 findings in
N prose line(s) of M in K file(s)`. A fence that never closes hides the rest
of the document from the scanner, so it is reported as its own high-severity
`unterminated_fence` finding rather than letting a barely-read file exit 0.
Layer 2 matches across the whole document, so a tell that straddles a hard
wrap is still caught. A blank line ends the match: the shapes are sentences,
not paragraphs.
The voice rule is discovered in this order: `--rules`, then
`$CLAUDE_PLUGIN_ROOT/rules/voice.md`, then
`$COPILOT_PLUGIN_ROOT/instructions/voice.instructions.md`, then
`.claude/rules/voice.md` or `.github/instructions/voice.instructions.md`
under the current directory, then `rules/voice.md` or
`instructions/voice.instructions.md` under the plugin install root (the
directory holding `.claude-plugin/plugin.json`). When no copy is reachable the script warns on stderr and runs the dash
and structural checks only, so a vendored install degrades instead of failing.
Exit codes (ADR-035):
- `0` no high-severity findings (`info` findings may still be present)
- `1` at least one high-severity finding
- `2` configuration error (a named file or the rules file cannot be read)
### burstiness.py
Layer 3 helper. Computes sentence-length variance (burstiness) and a
concreteness count (numbers, file paths, multi-word capitalized entities) for a
prose artifact. It is a proxy, not a gate.
Usage:
```bash
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/burstiness.py" FILE
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/burstiness.py" FILE --json
```
It prints sentence count, word count, mean and standard deviation of sentence
length, coefficient of variation, concreteness count, and a flat-rhythm warning
when variance is low and there are at least four sentences.
Exit codes (ADR-035):
- `0` analyzed successfully (with or without a flat-rhythm warning)
- `2` configuration or input error (missing file, unreadable path)
## Verification
```bash
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/prose_lint.py" FILE
echo "exit=$?" # 0 = Layers 1-2 clean, 1 = findings, 2 = bad input
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/prose-self-check/scripts/burstiness.py" FILE
echo "exit=$?" # 0 = analyzed, 2 = bad input
```
- [ ] `prose_lint.py` exits 0 (Layers 1 and 2 clean).
- [ ] Every `info` finding was adjudicated by Layer 4, not scrubbed on sight.
- [ ] No flat-rhythm warning, or a deliberate reason to keep the rhythm (Layer 3).
- [ ] Every paragraph makes a nameable, disagreeable claim (Layer 4).
The gate is your own four-layer pass, not the script. The script supports
Layer 3; Layers 1, 2, and 4 are judgment applied against `voice.md` and the
patterns above.
## Evidence
- John Carter, "I pulled ~90,000 Reddit posts about what makes writing sound
like AI (Part 2)," r/ClaudeCode, 2026-06-22 (`unslop-ai-text` scanner +
600-post hand-audit). Establishes the cited-vs-keyword divergence.
- Wikipedia: Signs of AI Writing (WikiProject AI Cleanup), 29-pattern catalog.
- Kobak et al. 2025 (arXiv 2406.07016), excess-vocabulary; "delves" +6,697%.
- Juzek & Ward, COLING 2025, traces lexical overrepresentation to RLHF.
## Related Skills
| Skill | Relationship |
|-------|--------------|
| [style-enforcement](../style-enforcement/SKILL.md) | Code style; this skill is prose only |
| [prompt-engineer](../prompt-engineer/SKILL.md) | Authors prompts; does not audit emitted prose |
| [doc-accuracy](../doc-accuracy/SKILL.md) | Doc factual accuracy; orthogonal to vernacular |
## References
- Banned vocabulary source of truth: `.claude/rules/voice.md` ("Banned
Vocabulary" section).
- Em-dash/en-dash MUST NOT: `.claude/rules/universal.md`.
- Skill standards: `.claude/skills/CLAUDE.md`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!