Generate and maintain documentation with Claude Code
Claude Code generates reference documentation (JSDoc and TSDoc comments, OpenAPI specifications, READMEs, architecture documents, and changelogs) by reading the source code rather than memory or an old wiki. Generation is the easy half. Hooks, deterministic gates such as TypeDoc validation and Redocly lint, and a headless drift check keep that documentation provably in step with the code.
This page is for developers who own a service’s docs and tech leads who want documentation to stop being a review burden. You know the situation: the README still mentions a command removed last sprint, openapi.yaml lists a field that was renamed, and new joiners ask the same questions because ARCHITECTURE.md describes the system from two refactors ago. Nobody updates docs by hand, so the fix has to be a workflow that regenerates them and a gate that fails when they drift.
What you’ll walk away with from documentation generation in Claude Code
Section titled “What you’ll walk away with from documentation generation in Claude Code”- A decision table: which docs Claude Code should generate, which it should only draft, and the check that proves each one.
- Copy-paste prompts for TSDoc, OpenAPI, a README whose commands are proven, and a cited
ARCHITECTURE.md. - A
docs:checkscript and aStophook that refuse to let a session end with broken docs. - A headless drift report for pull requests that returns structured JSON your CI can fail on.
Before you start, put your documentation conventions in CLAUDE.md (see the Claude Code memory system), so every session writes docs the same way.
Which documentation should Claude Code write?
Section titled “Which documentation should Claude Code write?”Claude Code is reliable wherever the answer is in the code. It is unreliable wherever the answer is in someone’s head. Decide per document type before you prompt:
| Document | Source of truth | Claude Code’s role | What proves it is right |
|---|---|---|---|
| TSDoc / JSDoc on exported symbols | Signatures, types, thrown errors | Generate | TypeDoc validation fails on any undocumented export |
| OpenAPI spec | Route handlers, validation schemas, middleware | Generate | Redocly lint plus a contract test that compares spec paths with registered routes |
| README setup section | Scripts that actually run | Generate, then execute | CI runs the README commands from a clean checkout |
ARCHITECTURE.md | Directory layout, entry points, data flow | Generate with citations | A script checks that every cited path exists |
| Changelog | git log between tags | Draft | The release owner edits the wording and signs off |
| Decision records (“why we chose X”) | People’s reasoning | Interview and draft only | The decision owner writes or approves every rationale |
The last row matters most. Asked “why does this module use a queue?”, Claude Code produces a plausible reason whether or not one exists. For rationale, use the interview workflow in architecture decisions agents can follow and let a human own the “why”.
Generate reference docs from the code
Section titled “Generate reference docs from the code”-
Add the gates to the repository first, so the first generated doc is already checked. For a TypeScript service:
Terminal window npm install -D typedoc @redocly/cliThen add a
docs:checkscript topackage.json:{"scripts": {"docs:check": "typedoc --emit none --entryPointStrategy expand --entryPoints src --validation.notDocumented --treatValidationWarningsAsErrors && redocly lint docs/openapi.yaml && bash scripts/check-doc-citations.sh"}}--emit nonevalidates without writing HTML. With--treatValidationWarningsAsErrors, TypeDoc 0.28 (checked with 0.28.20) exits non-zero when any exported symbol undersrc/has no documentation. -
Run
npm run docs:checkonce and save the failure list. That list is the backlog you hand to Claude Code. -
Paste the TSDoc prompt below into a Claude Code session, one directory at a time. A directory per session keeps each diff small enough to spot-check.
-
Run
npm run docs:checkagain. Green means every export is documented and the spec parses. It does not mean the prose is true, which is why the contract test and citation check below exist.
The “shorter than the function body” constraint is the cheapest fix for Claude Code’s main documentation habit, which is over-explaining. The @throws line is where generated docs beat hand-written ones: Claude Code follows the call chain and finds rethrown errors that a human author forgets.
Generate an OpenAPI spec that matches the running routes
Section titled “Generate an OpenAPI spec that matches the running routes”A spec generated from handlers alone misses what middleware adds: auth rejections, rate-limit headers, and the shape your error handler gives every failure. Name the middleware in the prompt, and ask for a test that ties the spec to the router, because a lint pass only proves the YAML is valid OpenAPI.
From then on, the contract test is the oracle: a route added without a spec entry turns CI red, whether a person or an agent added it. For the rest of the API workflow, see building REST and GraphQL APIs with Claude Code.
Write a README whose commands are proven
Section titled “Write a README whose commands are proven”A README fails new joiners in its commands, not its prose. Make Claude Code execute every command it writes, and make CI execute them again.
In CI, extract that one block and run it in a fresh container. The extractor reads only the first bash block inside the Quick start section and stops at the next ## heading, so a later section’s commands can never run by accident. A README that fails the job is a README that would have failed a new hire:
# CI step in a clean container: run the README's Quick start block, stop at the first failure.awk 'c&&/^```/{exit} c{print;next} /^## /{if(f)exit; f=($0 ~ /^## Quick start/); next} f&&/^```bash/{c=1}' README.md > /tmp/quickstart.shtest -s /tmp/quickstart.sh || { echo "README has no Quick start bash block"; exit 1; }bash -euo pipefail /tmp/quickstart.shGenerate an ARCHITECTURE.md you can check mechanically
Section titled “Generate an ARCHITECTURE.md you can check mechanically”Ask for citations in a fixed format, then verify the citations with a script instead of reading the prose line by line.
Save this as scripts/check-doc-citations.sh. It fails when a cited path no longer exists, which is how architecture docs usually rot:
#!/usr/bin/env bash# Fails if docs/ARCHITECTURE.md cites a path that does not exist.set -euo pipefailmissing=0while read -r path; do [[ -e $path ]] || { echo "missing: $path"; missing=1; }done < <(grep -oE '`[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)+/?(:[0-9]+)?`|`[A-Za-z0-9_.-]+\.(json|ya?ml|toml|md|lock|ts|js|mjs|py|go|rs|sql)(:[0-9]+)?`' docs/ARCHITECTURE.md \ | tr -d '`' | sed -E 's/:[0-9]+$//' | sort -u)exit $missingThe script checks every backticked token that contains a slash (under any directory, not only src/) and every root file with a common extension, such as package.json or pnpm-lock.yaml. A trailing :line is stripped before the check. The cost of that breadth: a backticked non-path such as request/response is reported as missing, so keep backticks for paths in this document.
The “Drift found” list is often the most useful output of the whole exercise. It is a free audit of every stale doc Claude Code read along the way. Understanding an unfamiliar codebase with Claude Code covers the same citation technique for exploration.
Draft a changelog from git history
Section titled “Draft a changelog from git history”The changelog stays a draft by design. Its wording is a promise to users, so the release owner signs it off.
Keep documentation in sync while Claude Code edits
Section titled “Keep documentation in sync while Claude Code edits”Generated docs rot the moment the next change lands. Two hooks close that gap inside the session. The mechanics of every hook event are in Claude Code hooks: guardrails, gates, and live context. Here are the two a documentation workflow needs.
A PostToolUse reminder that Claude Code actually sees. Plain echo output from a PostToolUse hook does not reach the model. To put the reminder in Claude Code’s context, return it as additionalContext:
#!/usr/bin/env bash# .claude/hooks/doc-drift-reminder.sh (PostToolUse, matcher "Edit|Write")set -uo pipefailfile=$(jq -r '.tool_input.file_path // empty')case "$file" in */src/routes/*|*/src/middleware/*) jq -nc '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: "You changed an API route or middleware. Before you finish, update docs/openapi.yaml if the request, response, auth or error contract changed."}}' ;;esacexit 0A Stop hook that runs the gate. The reminder is advice; this hook is the enforcement. It blocks the first attempt to finish while docs:check fails and hands over to a human if the retry also fails:
#!/usr/bin/env bash# .claude/hooks/docs-before-stop.sh (Stop)set -uo pipefailretrying=$(jq -r '.stop_hook_active')[[ -z $(git status --porcelain) ]] && exit 0if out=$(npm run --silent docs:check 2>&1); then exit 0; fiif [[ $retrying == "true" ]]; then jq -nc '{systemMessage: "docs:check still fails after one retry. Stopping for a human."}' exit 0fijq -nc --arg r "docs:check fails. Fix the docs, not the check. Last 30 lines:$(tail -n 30 <<<"$out")" '{decision: "block", reason: $r}'Register both in .claude/settings.json:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/doc-drift-reminder.sh", "args": [] }] } ], "Stop": [ { "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/docs-before-stop.sh", "args": [], "timeout": 180 }] } ] }}Run chmod +x .claude/hooks/*.sh, then open /hooks in a session to confirm both loaded.
Catch documentation drift in CI with headless Claude Code
Section titled “Catch documentation drift in CI with headless Claude Code”Hooks cover sessions that use them. Changes also arrive from people, from other agents, and from sessions without your settings, so the pull request needs its own check. The job below is read-only: it asks Claude Code whether the diff made any documentation stale and returns a validated JSON verdict that the script, not the model, turns into a pass or fail.
Save it as .github/workflows/docs-drift.yml:
name: docs-drifton: pull_requestpermissions: contents: readjobs: drift: # Fork PRs get no secrets; skip them instead of failing. if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v7 with: fetch-depth: 0 # the diff needs the base branch's history persist-credentials: false - uses: actions/setup-node@v7 with: node-version: 22 - run: npm install -g @anthropic-ai/claude-code - name: Report docs made stale by this diff env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASE_REF: ${{ github.base_ref }} run: | schema='{"type":"object","required":["stale"],"properties":{"stale":{"type":"array","items":{"type":"object","required":["doc","reason"],"properties":{"doc":{"type":"string"},"reason":{"type":"string"}}}}}}' out=$(claude -p "Run git diff --name-only origin/$BASE_REF...HEAD, then read the changed files and README.md, docs/openapi.yaml and docs/ARCHITECTURE.md. List every statement in those docs that this diff made false, with the doc path and the reason. Ignore wording and style. Return an empty list if nothing is stale." \ --bare --setting-sources "" --strict-mcp-config \ --permission-mode dontAsk --allowedTools "Bash(git diff *)" \ --output-format json --json-schema "$schema" --max-budget-usd 2) jq -e '.subtype == "success"' <<<"$out" >/dev/null || { echo "drift check did not complete"; exit 1; } if [ "$(jq '.structured_output.stale | length' <<<"$out")" -gt 0 ]; then jq -r '.structured_output.stale[] | "\(.doc): \(.reason)"' <<<"$out" exit 1 fiEvery flag was checked against claude --help 2.1.283 (the latest channel, 2026-09-26). fetch-depth: 0 matters: the default shallow checkout has no origin/main, so git diff fails and the job reports nothing useful. --bare --setting-sources "" --strict-mcp-config stops the pull request’s own hooks, settings, and .mcp.json from running in a job that holds a secret. Bare mode also skips your subscription login, which is why the job uses ANTHROPIC_API_KEY. dontAsk denies every tool call that would otherwise prompt, so the job can read the checkout and run git diff but cannot use the Edit or Write tools. The if: line skips pull requests from forks, which GitHub runs without secrets; do not switch to pull_request_target to get the key into them, because that runs the fork’s code next to your secret. The CI wiring and trigger choices are in Claude Code automation and pipeline configuration with Claude Code.
To regenerate docs after a merge, run the same kind of job on main with a narrow write grant: --allowedTools "Edit(./docs/**)" "Edit(./README.md)". Then have the script run npm run docs:check and upload the changes as a patch artifact; a separate job with contents: write and pull-requests: write, which runs no agent, applies the patch and opens the pull request. Keep persist-credentials: false in the agent’s job so no write token sits where the agent runs. Never let either job push to main.
On pull_request, GitHub runs docs-drift.yml from the pull request itself, so a pull request can edit or delete the check. Put .github/ under CODEOWNERS and require code-owner review before you treat this job as a merge gate.
Split a large documentation backlog across subagents
Section titled “Split a large documentation backlog across subagents”A repository-wide documentation pass fills the main context with file contents. Delegate each area to a subagent, so each one reads deeply and returns only a summary.
For reusable documentation specialists with their own tools and instructions, see custom subagents in Claude Code.
Which skills and MCP servers help with documentation?
Section titled “Which skills and MCP servers help with documentation?”Reach for a skill when you want a repeatable procedure the agent loads on demand. Reach for an MCP server when the agent needs live data it cannot read from the repository.
-
doc-coauthoring(Anthropic, anthropics/skills) runs a three-stage workflow of context gathering, refinement and structure, and reader testing. It suits the documents Claude Code must not invent alone: design docs, proposals, and decision records.Terminal window npx skills add anthropics/skills --skill doc-coauthoring -a claude-code -
Context7 (
@upstash/context7-mcp) pulls version-specific third-party library docs into context. Use it when your docs describe an integration, so an example matches the library version in your lockfile rather than the one in the model’s training data.Terminal window claude mcp add --scope user context7 -- npx -y @upstash/context7-mcp
The catalogues with more options are documentation and writing skills and documentation MCP servers such as Context7.
How do you verify generated docs without reading every line?
Section titled “How do you verify generated docs without reading every line?”Read only what no check can cover. Everything else has a gate, and the gate’s output goes into the pull request as evidence:
| Check | Command | Catches | Who signs off |
|---|---|---|---|
| Export coverage | npm run docs:check (TypeDoc step) | Undocumented or newly added exports | CI |
| Spec validity | npx @redocly/cli lint docs/openapi.yaml | Invalid OpenAPI, broken $refs | CI |
| Spec matches router | tests/contract/openapi-routes.test.ts | Missing or phantom endpoints | CI |
| README works | Quick-start block run in a clean container | Dead commands, missing prerequisites | CI |
| Architecture citations | scripts/check-doc-citations.sh | References to moved or deleted files | CI |
| Drift on this PR | Headless drift report | Docs made false by the diff | CI; the PR author fixes |
| Rationale and changelog wording | None | Invented reasons, misleading promises | Module owner, release owner |
The human review shrinks to the last row: the “why” sections and the changelog. That is where a person’s judgment is irreplaceable and where a generated sentence can do real harm. For the full list of what an agent’s pull request should carry, see the evidence bundle.
When generated documentation goes wrong
Section titled “When generated documentation goes wrong”The docs are long and say nothing. Claude Code restates the code in prose. Recovery: add “Keep each comment shorter than the function body” and “explain why and when to use it, not what each line does” to the prompt, and move the rule into CLAUDE.md so it applies in every session.
The OpenAPI spec lints clean but is wrong. Lint checks structure, not truth. The usual cause is middleware the prompt never mentioned. Recovery: name the middleware directory in the prompt, and let the contract test, not the lint, decide whether the spec matches the router.
Claude Code ignores the drift reminder. The hook prints with echo, and PostToolUse stdout does not reach the model. Recovery: return the message as hookSpecificOutput.additionalContext, as shown above, or exit 2 with the message on stderr.
The CI regeneration job succeeds and changes nothing. Every write was denied: -p sessions never prompt, and project permission rules may not apply in CI. Recovery: print .permission_denials from the JSON result and pass the exact Edit(...) rules with --allowedTools.
A decision record contains a confident, invented reason. The model filled a gap in the code with a plausible story. Recovery: tell Claude Code to write “reason not recorded” whenever the code, commit messages, or linked issues do not state the reason, then ask the decision owner to fill those gaps.
The stop hook loops or blocks forever. The check fails for a reason Claude Code cannot fix, such as a missing dev dependency. Recovery: the stop_hook_active branch above stops after one retry, and Claude Code itself ends the turn after eight consecutive stop-hook continuations (CLAUDE_CODE_STOP_HOOK_BLOCK_CAP changes the cap). If it still loops, run npm run docs:check yourself and fix the environment before you rerun.
The drift job fails with unknown revision or reports an empty diff. The checkout is shallow, so the base branch is missing. Recovery: set fetch-depth: 0 on actions/checkout, as in the workflow above, and confirm that git diff --name-only origin/main...HEAD lists files in a local clone of the PR branch.
Where to go next with documentation in Claude Code
Section titled “Where to go next with documentation in Claude Code”- Claude Code hooks: guardrails, gates, and live context covers every hook event and how to test hooks with fixtures.
- Scripting Claude Code: headless runs, pipes, cron and Git hooks is the next step for turning the drift report into a scheduled job.
- Using documentation as effective AI context is the other half: docs written for agents to read, not only for people.
- The same workflow in the other tools: documentation generation with Codex and auto-generating docs with Cursor.