Prose Editing skill for Claude Code

Cut a third, keep the meaning and the voice. Claude edits in separate passes from structure down to proofreading instead of rewriting everything at once. A bundled Python linter gives a baseline of filler, wordy phrases, hype words, likely passives and long sentences, and the edit ends with a before and after word count, a summary of changes and a list of queries wherever meaning was ambiguous, so the author's voice and facts stay intact.

  • nameprose-editing
  • size174 lines · ≈1,950 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Edits existing prose in separate passes — structure, cutting, sentence tightening, word choice, consistency, and correctness — so drafts get shorter and clearer without losing the author's meaning or voice. Includes a stdlib linter for filler, wordy phrases, hedges, hype words, likely passive voice, repeated words, long sentences, and a readability estimate. Use when asked to edit, tighten, proofread, shorten, or polish a draft, doc, email, blog post, or report, or when writing feels wordy, vague, or AI-generated.

Try asking

  • Tighten this design doc. It's too long and wordy but keep my voice
  • Proofread this blog post and flag anything ambiguous instead of guessing
  • This reads like AI wrote it. Edit it so it sounds like a person with opinions

Use it when

  • A draft needs editing, tightening, or proofreading
  • Writing feels wordy, vague, or generic
  • Preparing a document for a wider audience
  • Enforcing consistent terminology across a long document

Skip it when

How to check the result

Word count drops noticeably, the one-sentence main point is unchanged, linter findings fall to deliberate exceptions and every meaning change is listed as an author query.

The common mistake it prevents

Mechanically fixing every linter hit, which strips honest hedges and correct passive constructions and leaves prose that is shorter but less accurate.

~/.claude/skills/prose-editing/SKILL.md

--- name: prose-editing description: Edits existing prose in separate passes — structure, cutting, sentence tightening, word choice, consistency, and correctness — so drafts… license: MIT ---

An edit is done when the draft says the same thing (or more) in noticeably fewer words, every sentence has a clear actor and action, terms are consistent, nothing factual changed without the author knowing, and the author’s voice is still recognizable. An edit that rewrites everything into the editor’s style is a rewrite, not an edit.

Ground rules

  • Edit in passes, each looking for one class of problem. Trying to fix structure and commas at once misses both.
  • Big to small: structure → cuts → sentences → words → consistency → proofread.
  • Preserve meaning. When a sentence is ambiguous, ask or flag it; do not guess and silently “fix” it.
  • Preserve voice. Keep the author’s register, idioms, and humor unless they block clarity.
  • Show the changes for anything beyond light copyediting: a tracked diff, a summary of edit types, and a list of queries for the author.
  • Targets: first-draft technical or business prose usually loses 20–35% in a good edit.

Workflow

- [ ] 1. Read it all once without editing; write the main point in one sentence
- [ ] 2. Run the linter for a baseline
- [ ] 3. Pass 1: structure
- [ ] 4. Pass 2: cut
- [ ] 5. Pass 3: sentences
- [ ] 6. Pass 4: words
- [ ] 7. Pass 5: consistency and mechanics
- [ ] 8. Re-run linter; compare word count; write the edit summary and queries

2. Baseline

python3 scripts/prose_lint.py draft.md --long 30
wc -w draft.md

The linter skips code blocks, inline code, URLs, and front matter, and reports filler (just, simply, very), wordy phrases with replacements (in order to → to), hedges (arguably, somewhat), hype (seamless, robust, leverage), likely passive voice, doubled words, sentences over the length limit, and a Flesch reading-ease estimate. Treat findings as candidates, not verdicts: passive voice is sometimes right, and the heuristic misses some cases and flags some adjectives.

If the repo has Vale configured, run it too (vale draft.md).

3. Pass 1: structure

  • Does the opening state the point? If the real start is paragraph 2 or 3, cut or move what comes before.
  • Does each section earn its place and sit in the order the reader needs?
  • One idea per paragraph; topic sentence first.
  • Headings descriptive enough to skim (“Retries double-charge customers”, not “Background”).
  • Lists for parallel items; tables for comparisons; prose for reasoning.

4. Pass 2: cut

Cut on sight:

PatternExampleFix
Throat-clearing openers“It is worth noting that…” / “In this section we will discuss…”Delete; start with the claim
Restating the heading”## Caching / Caching is an important topic.”Delete the sentence
Redundant pairs“each and every”, “first and foremost”, “final outcome”“every”, “first”, “outcome”
Empty intensifiers“very important”, “really critical”, “extremely”Delete, or use a stronger specific word
Meta-commentary“As mentioned above…”, “As we will see…”Delete or link
Repeated conclusionsSummary paragraphs after a 300-word sectionDelete unless the doc is long
Hedge stacks“may possibly potentially”One hedge, if the uncertainty is real

5. Pass 3: sentences

ProblemBeforeAfter
Hidden actor (passive)“The request is rejected if the token is expired.”“The gateway rejects requests with expired tokens.”
Nominalization“We performed an investigation of the failure.”“We investigated the failure.”
Buried verb“Make a determination about whether to proceed.”“Decide whether to proceed.”
Long sentence“The service, which was migrated last quarter as part of the platform initiative that aimed to reduce costs, now handles…” (41 words)Split: “We migrated the service last quarter to cut costs. It now handles…”
Weak start“There are three reasons that cause this.”“Three things cause this.”
Negative phrasing“Do not forget to not skip validation.”“Always validate.”
Misplaced condition“Run the script if you use Docker.”“If you use Docker, run the script.”

Sentence length: vary it, average under ~20–25 words for technical prose, split anything over ~35 unless it is a deliberate list.

Passive voice is correct when the actor is unknown or irrelevant (“The file was deleted at 14:02”) or when the object is the topic; keep it there.

6. Pass 4: words

ReplaceWith
utilize, leverageuse
in order toto
due to the fact thatbecause
at this point in timenow
a number ofseveral / the actual number
facilitatehelp, enable, or the specific verb
functionalityfeature, or name it
robust, seamless, powerful, cutting-edgethe specific property, with evidence (“handles 10k req/s”)
impact (as a verb, vaguely)the specific effect (slows, breaks, raises)

Generic AI-sounding prose tells: “delve”, “in today’s fast-paced world”, “it’s important to note”, “a testament to”, “navigate the complexities”, tidy triplets of adjectives, every paragraph ending with a summary line. Replace them with concrete claims or delete.

7. Pass 5: consistency and mechanics

  • One term per concept (build a mini glossary as you go: “workspace” vs. “org”).
  • Consistent capitalization of product names and headings (sentence case unless the style guide says otherwise).
  • Numbers: numerals for measurements and anything technical; consistent units (ms vs. milliseconds) and thousands separators.
  • Lists: parallel grammar (all start with verbs, or all nouns), consistent end punctuation.
  • Dates and times in one format (ISO or the house style), timezone stated.
  • Links: descriptive link text, no “click here”.
  • Proofread last, ideally reading aloud or in a different format (rendered view), which surfaces errors the eye skips.

Edit levels (confirm which one the author wants)

LevelScopeTypical change
ProofreadTypos, grammar, punctuation< 5% of words
CopyeditPlus sentence clarity, consistency, word choice10–20%
Line editPlus restructuring sentences and paragraphs, cutting20–35%
DevelopmentalPlus structure, argument, what to add or removeAnything; deliver as notes first

If unspecified, default to a copyedit plus suggested cuts, and ask before structural changes.

Gotchas

  • Over-editing technical terms into “plainer” words that are wrong (“thread” → “process”).
  • Fixing every linter hit mechanically; some hedges are honest uncertainty and some passives are correct.
  • Losing required qualifiers in legal, security, or medical text: flag instead of cutting.
  • Changing quoted text, code, error messages, or UI labels; those must match the source exactly.
  • Homogenizing every sentence to the same length makes prose monotonous.

Verification

  • Word count dropped (report before/after) and the one-sentence main point is unchanged.
  • Linter findings re-run and reduced; remaining ones are deliberate.
  • Diff reviewed for meaning changes; every factual or ambiguous change is listed as a query.
  • Read the edited version start to finish once more in rendered form.

Output contract

## Edit summary
Level: line edit. Words: 1,840 → 1,260 (−32%). Linter findings: 47 → 6 (remaining are deliberate passives).
Main changes: moved conclusion to the top; cut background section to two sentences;
split 14 long sentences; unified "workspace" (was also "org", "team").

## Queries for the author
1. Para 3: "it usually completes quickly" — can we give a number?
2. Section 2: is the 99.9% figure monthly or annual?

## Edited text
<full text, or a unified diff for long documents>

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

scripts/prose_lint.py 169 lines · 7.1 KB
#!/usr/bin/env python3
"""Flag common prose problems in Markdown or plain text. Standard library only.

Usage:
  python3 prose_lint.py FILE [FILE ...] [--long 30] [--max-issues 0] [--quiet]

Checks (skips fenced code blocks, inline code, URLs, and front matter):
  filler     words that add nothing (just, simply, very, basically, actually ...)
  wordy      phrases with shorter equivalents (in order to -> to, utilize -> use ...)
  hedge      weasel words (arguably, somewhat, quite, fairly, it seems ...)
  passive    likely passive voice (be-verb + past participle); heuristic
  long       sentences over --long words
  repeat     the same word twice in a row ("the the")
  hype       marketing words (seamless, robust, leverage, cutting-edge ...)
Prints per-line findings, then sentence stats and a Flesch reading ease estimate.
Exit 1 when findings exceed --max-issues (default 0), 2 on usage errors.
"""
import argparse
import re
import sys
from pathlib import Path

FILLER = r"just|simply|very|really|basically|actually|literally|obviously|clearly|of course|totally|quite simply|needless to say"
HEDGE = r"arguably|somewhat|fairly|rather|quite|perhaps|it seems|sort of|kind of|more or less|to some extent"
HYPE = r"seamless(?:ly)?|robust|leverag(?:e|es|ed|ing)|cutting-edge|game-?changer|best-in-class|world-class|revolutionary|delve|unlock|elevate|supercharge|blazing(?:ly)? fast|next-gen"
WORDY = {
    r"in order to": "to",
    r"due to the fact that": "because",
    r"at this point in time": "now",
    r"in the event that": "if",
    r"for the purpose of": "to / for",
    r"has the ability to": "can",
    r"is able to": "can",
    r"utiliz(?:e|es|ed|ing)": "use",
    r"a number of": "some / several / <the number>",
    r"prior to": "before",
    r"in spite of the fact that": "although",
    r"with regard to|with respect to": "about",
    r"it is important to note that|please note that|note that": "(cut)",
    r"make a decision": "decide",
    r"provide assistance": "help",
    r"in terms of": "(rephrase)",
}
BE = r"(?:am|is|are|was|were|be|been|being)"
IRREGULAR = r"built|done|made|written|given|taken|seen|known|shown|sent|held|kept|left|lost|paid|put|read|run|set|told|thrown|brought|bought|caught|chosen|drawn|driven|eaten|fallen|found|forgotten|got|gotten|hidden|hit|hurt|led|meant|met|sold|shut|spent|split|spread|stolen|struck|taught|understood|won|worn"
PASSIVE = re.compile(rf"\b{BE}\s+(?:\w+ly\s+)?(\w+ed|{IRREGULAR})\b", re.I)
WORD = re.compile(r"[A-Za-z][A-Za-z'-]*")


def clean_lines(text):
    """Yield (line_no, text) with code, front matter, URLs, and HTML removed."""
    in_fence = False
    lines = text.splitlines()
    start = 0
    if lines and lines[0].strip() == "---":
        for i in range(1, len(lines)):
            if lines[i].strip() == "---":
                start = i + 1
                break
    for i, line in enumerate(lines[start:], start + 1):
        if re.match(r"^\s*(```|~~~)", line):
            in_fence = not in_fence
            continue
        if in_fence or re.match(r"^\s{4,}\S", line) or line.lstrip().startswith("|---"):
            continue
        line = re.sub(r"`[^`]*`", " ", line)
        line = re.sub(r"\]\([^)]*\)", "]", line)
        line = re.sub(r"https?://\S+", " ", line)
        line = re.sub(r"<[^>]+>", " ", line)
        yield i, line


def syllables(word):
    w = word.lower().strip("'")
    if len(w) <= 3:
        return 1
    w = re.sub(r"(?:[^laeiouy]es|ed|[^laeiouy]e)$", "", w)
    w = re.sub(r"^y", "", w)
    return max(1, len(re.findall(r"[aeiouy]{1,2}", w)))


def lint(path, long_limit):
    text = path.read_text(encoding="utf-8")
    findings = []
    prose = []
    for n, line in clean_lines(text):
        prose.append(line)
        checks = [
            ("filler", re.compile(rf"\b(?:{FILLER})\b", re.I), None),
            ("hedge", re.compile(rf"\b(?:{HEDGE})\b", re.I), None),
            ("hype", re.compile(rf"\b(?:{HYPE})\b", re.I), None),
        ]
        for kind, rx, _ in checks:
            for m in rx.finditer(line):
                findings.append((n, kind, m.group(0), ""))
        for pat, repl in WORDY.items():
            for m in re.finditer(rf"\b(?:{pat})\b", line, re.I):
                findings.append((n, "wordy", m.group(0), f"-> {repl}"))
        for m in PASSIVE.finditer(line):
            findings.append((n, "passive", m.group(0), "name the actor?"))
        for m in re.finditer(r"\b(\w+)\s+\1\b", line, re.I):
            if not m.group(1).isdigit():
                findings.append((n, "repeat", m.group(0), ""))

    # headings, list markers and table rows are not sentences; end them so they do not merge
    parts = []
    for line in prose:
        if re.match(r"^\s*#+\s", line) or line.lstrip().startswith("|"):
            continue
        line = re.sub(r"^\s*(?:[-*+]|\d+\.)\s+", "", line).strip()
        if line:
            parts.append(line if re.search(r"[.!?:]$", line) else line + ".")
    body = " ".join(parts)
    sentences = [s for s in re.split(r"(?<=[.!?])\s+(?=[A-Z\"'(])", body) if WORD.search(s)]
    lengths = []
    for s in sentences:
        words = WORD.findall(s)
        lengths.append(len(words))
        if len(words) > long_limit:
            snippet = " ".join(words[:8]) + " ..."
            findings.append((0, "long", f"{len(words)} words", snippet))

    words = WORD.findall(body)
    stats = {}
    if words and sentences:
        syl = sum(syllables(w) for w in words)
        stats = {
            "sentences": len(sentences),
            "avg_words": sum(lengths) / len(lengths),
            "max_words": max(lengths),
            "flesch": 206.835 - 1.015 * (len(words) / len(sentences)) - 84.6 * (syl / len(words)),
        }
    return findings, stats


def main():
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("files", nargs="+")
    p.add_argument("--long", type=int, default=30, help="flag sentences longer than this")
    p.add_argument("--max-issues", type=int, default=0)
    p.add_argument("--quiet", action="store_true", help="only print the summary")
    a = p.parse_args()

    total = 0
    for f in a.files:
        path = Path(f)
        if not path.is_file():
            print(f"error: {f} not found", file=sys.stderr)
            return 2
        findings, stats = lint(path, a.long)
        total += len(findings)
        print(f"== {path}")
        if not a.quiet:
            for n, kind, text, note in sorted(findings, key=lambda x: (x[0], x[1])):
                loc = f"L{n}" if n else "--"
                print(f"  {loc:<6}{kind:<8}{text}  {note}".rstrip())
        counts = {}
        for _, kind, _, _ in findings:
            counts[kind] = counts.get(kind, 0) + 1
        print(f"  findings: {len(findings)} " + (str(counts) if counts else ""))
        if stats:
            print(f"  sentences: {stats['sentences']}, avg {stats['avg_words']:.1f} words, "
                  f"longest {stats['max_words']}, Flesch reading ease ~{stats['flesch']:.0f} "
                  f"(60-70 plain English; technical docs often 40-60)")
    return 1 if total > a.max_issues else 0


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc