README Writing skill for Codex

From repo page to first result in five minutes. Codex reads the manifest, CI and env var usage before writing, so package names, runtime versions and required settings in the README match the code. It runs the install and quickstart in a clean venv or container and pastes the real output, then a bundled checker flags broken relative links, untagged code fences, placeholders and badge clutter.

  • Writing & Docs
  • $readme-writing
  • 189 lines
  • 3 files
  • ~1,900 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- readme-writing

Installs to ~/.agents/skills/readme-writing/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $readme-writing, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and repairs README.md files so a new user goes from landing on the repo to a working first result in about five minutes - one-line pitch, verified install and quickstart commands, a minimal usage example with real output, configuration, and links out for depth. Includes a stdlib Python checker for broken relative links, untagged code fences and missing sections. Use when creating a README for a new project, library or CLI, when the README is outdated or a wall of badges, or before open-sourcing or publishing a package.

Use it when

  • A new library, CLI or service needs its first README
  • The README is outdated, too long, or its quickstart no longer runs
  • You are about to open-source a repo or publish a package

Use something else when

  • You need a full documentation site with navigation and versioning: use docs-as-code
  • You are writing the --help output or man page for a CLI: use cli-help-text

How to check the result

scripts/check_readme.py exits 0 and the install plus quickstart commands were executed in a clean environment with the shown output copied from that run.

The mistake it prevents

A quickstart that only works on the author's machine because it silently depends on an env var, a running database or a globally installed tool.

~/.agents/skills/readme-writing/SKILL.md

--- name: readme-writing description: Writes and repairs README.md files so a new user goes from landing on the repo to a working first result in about five minutes - one-line pitch,… license: MIT ---

A README works when a stranger can answer “what is this, is it for me, how do I run it” from the first screen, and every command in it succeeds on a clean machine. It is a front door, not the whole house: depth lives in docs and is linked.

Inspect before writing

The README must describe the project as it is. Collect facts first:

cat AGENTS.md CONTRIBUTING.md 2>/dev/null | head -80   # house conventions, test command
ls; git log --oneline -5                                # layout, activity
cat package.json pyproject.toml Cargo.toml go.mod 2>/dev/null | head -60
ls .github/workflows 2>/dev/null                        # what CI actually runs
cat LICENSE 2>/dev/null | head -3

Record: package name as published (npm/PyPI/crates name may differ from the repo name), minimum runtime version (engines, requires-python, rust-version, go directive), entry points (bin, [project.scripts]), required env vars (grep for process.env, os.environ, os.Getenv), and the license identifier.

Classify the project

Project typeFirst code block showsMust also have
LibraryInstall, then a 5-15 line import-and-call example with outputSupported versions, link to API reference
CLI toolInstall, then one real command and its outputCommon flags table or --help excerpt
Application / serviceClone, configure, run locallyRequired env vars table, ports, data setup
Template / starterHow to create a new project from itWhat to rename, what to delete
Internal monorepo packageHow to run and test it inside the repoOwner team, where it is deployed

Section order

Use this order; drop sections that do not apply, never pad with empty ones.

  1. Name and one-line description. What it does, for whom, in under 20 words. No “a modern, blazing-fast…”. Say the noun: “A Python client for the Acme Billing API”.
  2. Badges (optional, at most 4). CI status, package version, license, coverage. More is noise.
  3. Why / when to use it (2-4 bullets). Include one bullet on when not to use it if there is an obvious alternative.
  4. Install. The exact command for the main package manager; alternatives below.
  5. Quickstart. The smallest end-to-end example that produces visible output. Show the output.
  6. Usage. Two to five common tasks, each a heading plus a code block.
  7. Configuration. Table of env vars or options: name, default, required, meaning.
  8. Documentation. Links to full docs, API reference, examples folder.
  9. Contributing / development. How to run tests locally, or a link to CONTRIBUTING.md.
  10. License. One line with the SPDX identifier and a link to LICENSE.

Optional when they earn their place: a short GIF or screenshot for visual tools (under ~2 MB, with alt text), a compatibility matrix, a FAQ of real questions from issues, security reporting (or link to SECURITY.md).

Workflow

- [ ] 1. Gather facts (commands above); list runtime versions and env vars
- [ ] 2. Write the one-line description and the quickstart first
- [ ] 3. Run install + quickstart in a clean environment; paste real output
- [ ] 4. Fill remaining sections from the order above
- [ ] 5. Run scripts/check_readme.py and fix every finding
- [ ] 6. Read the first screen (about 25 lines rendered) cold: does it answer what/who/how?

Proving the quickstart

Run it where cached state cannot hide problems:

# Python
python3 -m venv /tmp/rdm && . /tmp/rdm/bin/activate && pip install -e . && python examples/quickstart.py
# Node
cd "$(mktemp -d)" && npm init -y >/dev/null && npm install /path/to/repo && node -e "require('pkg')"
# Containerised, closest to a stranger's machine
docker run --rm -v "$PWD":/src -w /src python:3.12-slim sh -c "pip install . && mytool --version"

Package installs need network; in a Codex sandbox with network off, ask for approval or state in the report that the quickstart was not executed. Never present unexecuted commands as verified.

Writing rules

  • Commands are copy-pasteable: no $ prompt prefix inside blocks that users copy, or use console blocks consistently with prompts and output.
  • Every code block has a language tag (bash, python, toml, text for output).
  • Placeholders are obvious and explained: export ACME_API_KEY=<your key from dashboard>.
  • State the minimum runtime: “Requires Python 3.10+” right at Install.
  • Prefer one recommended install path; put alternatives (Homebrew, binaries, from source) in a collapsed <details> block or a later section.
  • Relative links for files in the repo (docs/config.md), so they work on forks and in package registries that render them. Note: npm and PyPI render relative image paths poorly; use absolute raw URLs for images in published package READMEs.
  • Keep it under roughly 300 lines. Move long reference material to docs/.

Example skeleton

# acme-billing

Python client for the Acme Billing API: typed models, automatic retries and pagination.

[![CI](https://github.com/acme/acme-billing/actions/workflows/ci.yml/badge.svg)](https://github.com/acme/acme-billing/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/acme-billing)](https://pypi.org/project/acme-billing/)

## Install

Requires Python 3.10+.

```bash
pip install acme-billing
```

## Quickstart

```python
from acme_billing import Client

client = Client(api_key="sk_test_...")
invoice = client.invoices.get("inv_123")
print(invoice.total, invoice.currency)
```

```text
4200 EUR
```

## Configuration

| Variable | Default | Required | Meaning |
|---|---|---|---|
| `ACME_API_KEY` | none | yes | Secret key from the dashboard |
| `ACME_TIMEOUT` | `10` | no | Request timeout in seconds |

## License

MIT, see [LICENSE](LICENSE).

Checker script

python3 scripts/check_readme.py README.md            # human-readable findings
python3 scripts/check_readme.py README.md --strict   # warnings also fail (exit 1)

It reports broken relative links and images, code fences without a language, missing title, missing install/usage sections, leftover TODO/lorem placeholders, too many badges, and a first code block that starts too far down. It does not fetch external URLs; for those run lychee README.md when network is available.

Gotchas

  • Install command uses the repo name while the published package name differs.
  • Quickstart silently depends on an env var or a running database that the README never mentions. Run it in a clean shell to catch this.
  • Badges pointing at a deleted workflow file render as broken images forever.
  • Copying --help output by hand drifts; regenerate it from the tool at write time.
  • README duplicated into a docs site and edited in two places. Make one the source and include or link the other.
  • Do not invent features, benchmarks or roadmap promises the code does not support.

Verification

  • python3 scripts/check_readme.py README.md exits 0.
  • Install and quickstart executed in a clean environment in this session, with the pasted output matching what you saw.
  • The first rendered screen states what it is, who it is for, and how to install.
  • If the project publishes to a registry, preview how it renders there (relative image paths are the usual breakage).

Report

**README:** rewritten (212 lines, was 540)
**Quickstart verified:** yes, python:3.12-slim container, output pasted from run
**Checker:** 0 errors, 0 warnings
**Moved out:** full option reference to docs/configuration.md
**Needs owner input:** support policy for Python 3.9 (README now says 3.10+)

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "README Writing"
  short_description: "From repo page to first result in five minutes"
  default_prompt: "$readme-writing Write a README for this CLI before I publish it to npm"
scripts/check_readme.py120 lines, 5.1 KB
#!/usr/bin/env python3
"""Static checks for a README.md: structure, code fences, relative links, placeholders.

Usage: python3 check_readme.py README.md [--strict]
Exit codes: 0 clean (or warnings only), 1 errors found (or warnings with --strict), 2 usage error.
Standard library only; never touches the network.
"""
from __future__ import annotations

import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse

INSTALL_WORDS = ("install", "getting started", "setup", "set up", "quickstart", "quick start")
USAGE_WORDS = ("usage", "quickstart", "quick start", "example", "getting started", "how to use")
PLACEHOLDERS = re.compile(r"\b(TODO|TBD|FIXME|lorem ipsum|your-project-name|project-name-here)\b", re.I)
LINK = re.compile(r"!?\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+\"[^\"]*\")?\s*\)")
REF_DEF = re.compile(r"^\s*\[[^\]]+\]:\s*(\S+)", re.M)
HTML_SRC = re.compile(r"""(?:src|href)\s*=\s*["']([^"']+)["']""", re.I)
BADGE = re.compile(r"\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)")


def strip_code(text: str) -> tuple[str, list[tuple[int, str]]]:
    """Return text with fenced blocks blanked, plus (line_no, info_string) for each fence opener."""
    out, fences, in_fence, marker = [], [], False, ""
    for n, line in enumerate(text.splitlines(), start=1):
        m = re.match(r"^\s{0,3}(`{3,}|~{3,})(.*)$", line)
        if m and not in_fence:
            in_fence, marker = True, m.group(1)[0] * len(m.group(1))
            fences.append((n, m.group(2).strip()))
            out.append("")
            continue
        if in_fence and line.strip().startswith(marker):
            in_fence = False
            out.append("")
            continue
        out.append("" if in_fence else line)
    return "\n".join(out), fences


def main(argv: list[str]) -> int:
    args = [a for a in argv if not a.startswith("--")]
    strict = "--strict" in argv
    if len(args) != 1:
        print(__doc__.strip(), file=sys.stderr)
        return 2
    path = Path(args[0])
    if not path.is_file():
        print(f"error: {path} not found", file=sys.stderr)
        return 2

    text = path.read_text(encoding="utf-8")
    prose, fences = strip_code(text)
    lines = prose.splitlines()
    errors: list[str] = []
    warnings: list[str] = []

    headings = [(i + 1, l.lstrip("#").strip().lower()) for i, l in enumerate(lines) if re.match(r"^#{1,6}\s", l)]
    h1 = [h for h in headings if re.match(r"^#\s", lines[h[0] - 1])]
    if not h1 and not re.search(r"<h1", text, re.I):
        errors.append("no H1 title (# Project name) found")
    if not any(any(w in h for w in INSTALL_WORDS) for _, h in headings):
        warnings.append("no Install / Getting started heading")
    if not any(any(w in h for w in USAGE_WORDS) for _, h in headings):
        warnings.append("no Usage / Quickstart / Example heading")
    if not re.search(r"licen[cs]e", text, re.I):
        warnings.append("license is never mentioned")

    for n, info in fences:
        if not info:
            warnings.append(f"line {n}: code fence has no language tag")
    if fences and fences[0][0] > 80:
        warnings.append(f"first code block starts at line {fences[0][0]}; readers want a command within the first screen")
    if not fences:
        warnings.append("no code blocks: README shows no install or usage command")

    badges = len(BADGE.findall(prose))
    if badges > 4:
        warnings.append(f"{badges} badges; keep at most 4 that carry real signal")

    for n, line in enumerate(lines, start=1):
        m = PLACEHOLDERS.search(line)
        if m:
            errors.append(f"line {n}: placeholder text '{m.group(0)}'")

    targets = [(m.group(1), prose[: m.start()].count("\n") + 1) for m in LINK.finditer(prose)]
    targets += [(m.group(1), prose[: m.start()].count("\n") + 1) for m in REF_DEF.finditer(prose)]
    targets += [(m.group(1), prose[: m.start()].count("\n") + 1) for m in HTML_SRC.finditer(prose)]
    base = path.resolve().parent
    anchors = {re.sub(r"[^\w\- ]", "", h).strip().replace(" ", "-") for _, h in headings}
    for target, n in targets:
        parsed = urlparse(target)
        if parsed.scheme in ("http", "https", "mailto", "data") or target.startswith("//"):
            continue
        if target.startswith("#"):
            if target[1:].lower() not in anchors:
                warnings.append(f"line {n}: anchor {target} matches no heading")
            continue
        rel = unquote(parsed.path)
        if not rel:
            continue
        candidate = (base / rel.lstrip("/")) if rel.startswith("/") else (base / rel)
        if not candidate.exists():
            errors.append(f"line {n}: relative link target missing: {target}")

    total = len(text.splitlines())
    if total > 400:
        warnings.append(f"{total} lines; consider moving reference material into docs/")

    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), {len(fences)} code block(s), {total} lines")
    return 1 if errors or (strict and warnings) else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem