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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- readme-writing Installs to ~/.agents/skills/readme-writing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install readme-writing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project readme-writing Installs to .agents/skills/readme-writing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/readme-writing.zipunzip -o readme-writing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) readme-writing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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 type | First code block shows | Must also have |
|---|---|---|
| Library | Install, then a 5-15 line import-and-call example with output | Supported versions, link to API reference |
| CLI tool | Install, then one real command and its output | Common flags table or --help excerpt |
| Application / service | Clone, configure, run locally | Required env vars table, ports, data setup |
| Template / starter | How to create a new project from it | What to rename, what to delete |
| Internal monorepo package | How to run and test it inside the repo | Owner team, where it is deployed |
Section order
Use this order; drop sections that do not apply, never pad with empty ones.
- 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”.
- Badges (optional, at most 4). CI status, package version, license, coverage. More is noise.
- Why / when to use it (2-4 bullets). Include one bullet on when not to use it if there is an obvious alternative.
- Install. The exact command for the main package manager; alternatives below.
- Quickstart. The smallest end-to-end example that produces visible output. Show the output.
- Usage. Two to five common tasks, each a heading plus a code block.
- Configuration. Table of env vars or options: name, default, required, meaning.
- Documentation. Links to full docs, API reference, examples folder.
- Contributing / development. How to run tests locally, or a link to CONTRIBUTING.md.
- 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 useconsoleblocks consistently with prompts and output. - Every code block has a language tag (
bash,python,toml,textfor 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.
[](https://github.com/acme/acme-billing/actions/workflows/ci.yml)
[](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
--helpoutput 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.mdexits 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.yaml
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.py
#!/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.