Skip to content
Back to skills

Prose Self Check

ASecurity

Pre-emit AI-vernacular self-check an agent runs on its OWN prose

  • 47 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 1, 2026
ai-agentspythonrustgobashgitapi

Works with

  • api

Security analysis

A100/100

Pro scans all 6 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add rjmurillo/ai-agents --skill prose-self-check --agent claude-code

Installs 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 with every re-scan.

Security grade badge for Prose Self Check
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rjmurillo-prose-self-check/badge)](https://www.skillsdirectory.com/skills/rjmurillo-prose-self-check)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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
metadata:
  routing:
    role: conditional-adjunct
    invoker: spec-generator
    trigger: spec-generator runs prose-self-check before emitting any prose artifact
    user-facing: false
---

# 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/skillforge/references/skill-development-conventions.md`.

Files in this skill

  • SKILL.md12.6 KB
  • scripts/burstiness.py5.3 KB
  • scripts/prose_lint.py63.6 KB
  • tests/test_burstiness.py4.4 KB
  • tests/test_skill_structure.py8.9 KB
  • tests/test_wiring.py3.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.