Technical Writing Editor's pick

README Writer skill for Claude Code

From landing on the repo to a working result. Claude reads your manifest, CLI help and config schema before writing so every flag and version in the README is real. It leads with a plain one-sentence pitch, keeps badges to the few users act on, writes a quickstart that reaches visible output in three commands or fewer, and runs a bundled checker that catches broken relative links, untagged code blocks and leftover TODOs.

  • namereadme-writer
  • size180 lines · ≈1,600 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: Writes and rewrites project READMEs that get a new user from "what is this?" to a working result in minutes — one-sentence pitch, verified install and quickstart, a real usage example with output, configuration table, and pointers to docs and contributing — with badges used sparingly. Includes a stdlib checker for missing sections, broken relative links, untagged code blocks, and leftover placeholders. Use when creating a README, improving an open-source project page, or when users open issues asking how to install or use the project.

Try asking

  • Write a README for this CLI tool based on the code
  • People keep opening issues asking how to install this. Fix the README
  • Trim our README down. It's 900 lines and nobody reads past the badges

Use it when

  • Creating a README for a new project or package
  • Users cannot get started from the current README
  • Preparing a repo for open-source release
  • Documenting an internal service for new teammates

Skip it when

  • You need a full documentation website, use docs-site instead
  • You are writing endpoint reference for an HTTP API, use api-documentation instead

How to check the result

scripts/readme_check.py exits 0, and the quickstart runs exactly as written in a fresh container and prints the documented output.

The common mistake it prevents

Shipping a quickstart that was never run on a clean machine, so it silently depends on a global tool, env var or login the author already had.

~/.claude/skills/readme-writer/SKILL.md

--- name: readme-writer description: Writes and rewrites project READMEs that get a new user from "what is this?" to a working result in minutes — one-sentence pitch, verified… license: MIT ---

A README is done when a stranger who lands on the repo can answer “what is this, is it for me, how do I try it” from the first screen, and can copy the quickstart into a clean machine and see the promised result. Everything else is secondary and can be linked.

Ground rules

  • Order by the visitor’s questions: what → why/for whom → install → first result → more usage → configuration → help/contributing → license.
  • The quickstart is tested in a clean environment (fresh container or new clone). An untested quickstart is the most common reason projects get “doesn’t work” issues.
  • Show, then tell: a code sample with real output beats a paragraph of features.
  • Keep it scannable: the README is a landing page, not the manual. Past ~400 lines, move material to docs/ and link.
  • Write for the user of the project, not its maintainers. Maintainer notes go in CONTRIBUTING.md.

Workflow

- [ ] 1. Identify project type and primary user
- [ ] 2. Collect facts from the repo (package manifest, CLI help, config schema, CI)
- [ ] 3. Write the top: name, one-line pitch, optional visual
- [ ] 4. Write install + quickstart and run them in a clean environment
- [ ] 5. Add usage, configuration, and links
- [ ] 6. Run scripts/readme_check.py; fix errors

2. Collect facts, do not invent them

cat package.json | head -40          # name, bin, engines, scripts
cat pyproject.toml 2>/dev/null       # requires-python, scripts, extras
ls .github/workflows/ 2>/dev/null    # which badges are real
<cli> --help                         # actual commands and flags
git describe --tags --abbrev=0       # current version

Take supported runtime versions from engines / requires-python, commands from the real --help, and config keys from the schema or source. Never document a flag that is not in the code.

3. The top of the README

# fotosort

Rename and file photos by the date they were taken, using EXIF data. Works offline on
macOS, Linux, and Windows.

![fotosort renaming a folder of photos in the terminal](docs/demo.gif)
  • One sentence: what it does, for whom or in what context. No “blazingly fast”, “modern”, “powerful”.
  • Optional second sentence: the key differentiator.
  • Visual (screenshot or short GIF under ~2 MB) for anything with a UI or terminal output.
  • Badges: 0–4 that users act on — CI status, latest version, license, maybe downloads/coverage. Not a wall of shields.

Before / after for the pitch:

BeforeAfter
“A powerful, flexible, and modern solution for managing your media.”“Rename and file photos by the date they were taken, using EXIF data.”
“Next-gen state management.”“A 2 KB React state library that stores state in the URL so views are shareable.”

4. Install and quickstart

## Install

Requires Node.js 20 or later.

```bash
npm install -g fotosort
```

## Quickstart

```bash
fotosort ./camera-dump --into ./photos
```

```text
Scanned 214 files
Moved 208 → photos/2026/10/
Skipped 6 (no EXIF date; listed in fotosort.log)
```
  • State prerequisites with versions before the install command.
  • One recommended install path first; alternatives (Homebrew, Docker, from source) after, briefly.
  • The quickstart reaches a visible result in 1–3 commands.
  • Show expected output so users know it worked.
  • Use placeholders consistently and say where values come from: export FOTOSORT_TOKEN=<token from https://example.com/settings>.
  • Usage: 2–4 common tasks, each a heading + command + one-line explanation. Link to full docs for the rest.
  • Configuration as a table:
| Option | Env var | Default | Description |
|---|---|---|---|
| `--into <dir>` | `FOTOSORT_INTO` | `./sorted` | Destination root |
| `--pattern <fmt>` | `FOTOSORT_PATTERN` | `{yyyy}/{mm}` | Folder pattern |
  • Help: where to ask questions (Discussions, chat), how to report bugs, security contact (link SECURITY.md).
  • Contributing: one line linking CONTRIBUTING.md plus the dev setup command if short.
  • License: name it and link the file.

Variants by project type

TypeMust includeUsually skip
LibraryInstall, minimal import + call example, link to API reference, supported versionsScreenshots
CLIInstall, quickstart with output, command table or --help excerptLong API sections
Web app / serviceWhat it does, screenshot, run locally (env vars, docker compose up), deploy linkFull API
Internal repoPurpose, owners, how to run/test/deploy, links to runbooks and dashboardsBadges, marketing
Monorepo rootWhat lives where (table of packages with one-line purpose), common commandsPer-package detail

Gotchas

  • Copy-paste traps: $ prompts inside code blocks break copying; put commands without prompts and output in a separate block.
  • Relative links and images break on npm/PyPI pages; use absolute URLs for images that must render there.
  • Stale versions in examples (“pip install tool==1.2”) drift; prefer unpinned installs in the README, pin in docs where needed.
  • A table of contents is only worth it past ~6 sections; GitHub renders one automatically from headings.
  • Emoji-laden headings and decorative badges make the page look generated and harder to scan.
  • Do not paste the whole --help output; summarize the 5 commands people use and link.

Verification

python3 scripts/readme_check.py README.md

The script flags a missing H1, missing install/quickstart/usage section, broken relative links and images, leftover placeholders (TODO, TBD, <your-...>), untagged code blocks, images without alt text, and badge overload. Exit code 1 on errors.

Then:

  • Run the quickstart in a clean environment (e.g. docker run --rm -it node:20 bash) exactly as written; fix the README for every deviation.
  • Read only the first screen (about 25 lines): is “what, for whom, how to try” answered?
  • Check that every documented flag exists in --help output.

Output contract

# <name>
<one-sentence pitch> <optional differentiator>
[visual]  [≤ 4 badges]

## Install          prerequisites + one command
## Quickstart       1–3 commands + expected output
## Usage            2–4 common tasks
## Configuration    table
## Documentation    links
## Contributing     link to CONTRIBUTING.md
## License          name + link

Bundled files

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

scripts/readme_check.py 103 lines · 3.8 KB
#!/usr/bin/env python3
"""Lint a README for the problems that most often stop users from getting started.

Usage:
  python3 readme_check.py [README.md] [--max-badges 6]

Errors (exit 1):
  - no H1 title
  - no install / quickstart / getting started / usage section
  - relative links or images that point to files that do not exist
  - placeholder text left in (TODO, TBD, lorem ipsum, <your-...>, FIXME)
Warnings:
  - fenced code blocks without a language tag
  - images without alt text
  - more badges than --max-badges
  - first paragraph missing or longer than 60 words
  - no license mention
Standard library only.
"""
import argparse
import re
import sys
from pathlib import Path

SECTION_RE = re.compile(r"^#{2,3}\s+(install|installation|quick ?start|getting started|usage|setup)\b", re.I | re.M)
LINK_RE = re.compile(r"(!?)\[([^\]]*)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
PLACEHOLDER_RE = re.compile(r"\b(TODO|TBD|FIXME|lorem ipsum)\b|<your[-_ ][^>]+>", re.I)
BADGE_RE = re.compile(r"!\[[^\]]*\]\(https?://(img\.shields\.io|badge\.fury\.io|github\.com/[^)]+/badge\.svg|codecov\.io|[^)]*badge)[^)]*\)", re.I)


def strip_code(text):
    return re.sub(r"```.*?```", "", text, flags=re.S)


def main():
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("path", nargs="?", default="README.md")
    p.add_argument("--max-badges", type=int, default=6)
    a = p.parse_args()

    path = Path(a.path)
    if not path.is_file():
        print(f"error: {path} not found", file=sys.stderr)
        return 2
    text = path.read_text(encoding="utf-8")
    prose = strip_code(text)
    errors, warnings = [], []

    if not re.search(r"^#\s+\S", prose, re.M) and not re.search(r"<h1[\s>]", prose, re.I):
        errors.append("no H1 title")
    if not SECTION_RE.search(prose):
        errors.append("no Install / Quickstart / Getting started / Usage section (## level)")

    in_fence = False
    for lineno, line in enumerate(text.splitlines(), 1):
        if line.lstrip().startswith("```"):
            in_fence = not in_fence
            continue
        if not in_fence:
            for m in PLACEHOLDER_RE.finditer(line):
                errors.append(f"placeholder '{m.group(0)}' on line {lineno}")

    for bang, alt, target in LINK_RE.findall(prose):
        if bang and not alt.strip():
            warnings.append(f"image without alt text: {target}")
        if re.match(r"^(https?:|mailto:|#|data:)", target):
            continue
        local = (path.parent / target.split("#", 1)[0]).resolve()
        if target.split("#", 1)[0] and not local.exists():
            errors.append(f"broken relative {'image' if bang else 'link'}: {target}")

    fences = re.findall(r"^```(.*)$", text, re.M)
    openers = fences[0::2]
    untagged = sum(1 for f in openers if not f.strip())
    if untagged:
        warnings.append(f"{untagged} code block(s) without a language tag")

    badges = len(BADGE_RE.findall(text))
    if badges > a.max_badges:
        warnings.append(f"{badges} badges (> {a.max_badges}); keep the ones users act on")

    body = re.sub(r"^\s*(#.*|!\[.*|<.*>|\[!\[.*)\s*$", "", prose, flags=re.M)
    paras = [x.strip() for x in re.split(r"\n\s*\n", body) if x.strip()]
    if not paras:
        warnings.append("no introductory paragraph")
    else:
        words = len(paras[0].split())
        if words > 60:
            warnings.append(f"first paragraph is {words} words; say what it is and who it is for in under 60")

    if not re.search(r"licen[cs]e", text, re.I) and not (path.parent / "LICENSE").exists():
        warnings.append("no license mention and no LICENSE file")

    for e in errors:
        print(f"ERROR  {e}")
    for w in warnings:
        print(f"warn   {w}")
    print(f"\n{path}: {len(errors)} error(s), {len(warnings)} warning(s)")
    return 1 if errors 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