# Generate and maintain documentation with Claude Code

URL: https://developertoolkit.ai/en/claude-code/build/documentation/
Description: Generate JSDoc, OpenAPI, READMEs and architecture docs from code with Claude Code, then keep them true with hooks, lint gates and a headless drift check.

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

- 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:check` script and a `Stop` hook 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](/en/claude-code/build/memory-system/)), so every session writes docs the same way.

## 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](/en/workflows/plan/architecture-decisions/) and let a human own the "why".

## Generate reference docs from the code

1. Add the gates to the repository first, so the first generated doc is already checked. For a TypeScript service:

   ```bash
   npm install -D typedoc @redocly/cli
   ```

   Then add a `docs:check` script to `package.json`:

   ```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 none` validates without writing HTML. With `--treatValidationWarningsAsErrors`, TypeDoc 0.28 (checked with 0.28.20) exits non-zero when any exported symbol under `src/` has no documentation.

2. Run `npm run docs:check` once and save the failure list. That list is the backlog you hand to Claude Code.

3. 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.

4. Run `npm run docs:check` again. 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.

**Copy-paste prompt: TSDoc for one directory**

```text
Run `npm run docs:check` and read the TypeDoc warnings for src/services/.
Add TSDoc to every exported function, class and type listed there:
- one-sentence summary of what it does, not how
- @param for each parameter, stating units and allowed ranges where the code enforces them
- @returns, and @throws for every error the function throws or rethrows
- @example only when the call is non-obvious (options objects, callbacks, retries)
Follow the style already used in src/lib/database.ts. Do not change any code.
Keep each comment shorter than the function body it documents.
When done, run `npm run docs:check` and show me the remaining warnings.
```

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

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.

**Copy-paste prompt: OpenAPI 3.1 spec with a contract test**

```text
Generate docs/openapi.yaml (OpenAPI 3.1) from the route handlers in src/routes/.
Derive request and response schemas from the Zod schemas each handler uses.
Before writing, read the middleware chain in src/middleware/: record which routes
require auth (401/403 responses), which add rate-limit headers, and the error
shape src/middleware/error-handler.ts returns, and document all of it.
Then write tests/contract/openapi-routes.test.ts that loads the spec, lists every
route registered on the router, and fails if a route is missing from the spec or
the spec lists a path the router does not serve.
Run `npx @redocly/cli lint docs/openapi.yaml` and the new test. Report both results.
```

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](/en/claude-code/build/api-development/).

### 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.

**Copy-paste prompt: README with executed commands**

```text
Rewrite README.md for a developer on their first day. Sections: what the
service does (one paragraph, no marketing), prerequisites with exact versions
read from .node-version and package.json, setup, run, test, environment
variables (names and purpose only, never values), and project layout.
Run every command you put in a code block, in order, from a clean state.
If a command fails, fix the README, not the code, and tell me what was wrong.
Put all the setup-to-test commands in exactly one fenced block tagged `bash`
under "## Quick start", the first bash block in that section, so CI can
extract and run them.
```

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:

```bash
# 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.sh
test -s /tmp/quickstart.sh || { echo "README has no Quick start bash block"; exit 1; }
bash -euo pipefail /tmp/quickstart.sh
```

### 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.

**Copy-paste prompt: cited architecture document**

```text
Write docs/ARCHITECTURE.md, under 200 lines: system overview (one paragraph),
tech stack with versions from the lockfile, directory map, the request
lifecycle, background jobs, external services, and how auth works end to end.
Cite a file for every claim as `path/to/file.ts` in backticks, using paths
relative to the repo root. Mark anything you inferred rather than read as
(inferred). Where code and existing docs disagree, trust the code and list
each disagreement at the end under "Drift found".
```

Save this as `scripts/check-doc-citations.sh`. It fails when a cited path no longer exists, which is how architecture docs usually rot:

```bash
#!/usr/bin/env bash
# Fails if docs/ARCHITECTURE.md cites a path that does not exist.
set -euo pipefail
missing=0
while 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 $missing
```

The 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](/en/claude-code/plan/codebase-analysis/) covers the same citation technique for exploration.

### Draft a changelog from git history

**Copy-paste prompt: changelog entry for the release owner**

```text
Read `git log --no-merges $(git describe --tags --abbrev=0)..HEAD` and the
diffs of any commit whose message is vague. Draft a CHANGELOG.md entry in
Keep a Changelog format (Added, Changed, Fixed, Removed). Write for users of
the API, not its maintainers: "Fixed rounding in multi-currency invoices",
not "fix null check in invoice.service.ts". Put every breaking change first,
with the migration step. Do not edit CHANGELOG.md; print the draft so I can
review it.
```

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

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](/en/claude-code/automate/hooks-automation/). 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`:

```bash
#!/usr/bin/env bash
# .claude/hooks/doc-drift-reminder.sh (PostToolUse, matcher "Edit|Write")
set -uo pipefail
file=$(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."}}' ;;
esac
exit 0
```

**A `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:

```bash
#!/usr/bin/env bash
# .claude/hooks/docs-before-stop.sh (Stop)
set -uo pipefail
retrying=$(jq -r '.stop_hook_active')
[[ -z $(git status --porcelain) ]] && exit 0
if out=$(npm run --silent docs:check 2>&1); then exit 0; fi
if [[ $retrying == "true" ]]; then
  jq -nc '{systemMessage: "docs:check still fails after one retry. Stopping for a human."}'
  exit 0
fi
jq -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`:

```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

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`:

```yaml
name: docs-drift
on: pull_request
permissions:
  contents: read
jobs:
  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
          fi
```

Every 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](/en/claude-code/automate/automation/) and [pipeline configuration with Claude Code](/en/claude-code/ship/ci-cd/).

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

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.

**Copy-paste prompt: parallel documentation pass**

```text
Use three subagents in parallel, each working only in its own paths:
1. TSDoc for every export in src/lib/ that `npm run docs:check` flags.
2. docs/database.md from the Drizzle schema in src/db/schema.ts: each table,
   its relationships, and the reason for every index (mark unknown reasons
   as "reason not recorded" instead of guessing).
3. docs/testing.md: our test layout, how to run one suite, and how to add a test.
Each subagent runs `npm run docs:check` on its own output before returning.
Return a summary per subagent: files changed, checks run, open questions.
```

For reusable documentation specialists with their own tools and instructions, see [custom subagents in Claude Code](/en/claude-code/automate/custom-subagents/).

## 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](https://github.com/anthropics/skills/tree/main/skills/doc-coauthoring)) 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.

  ```bash
  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.

  ```bash
  claude mcp add --scope user context7 -- npx -y @upstash/context7-mcp
  ```

The catalogues with more options are [documentation and writing skills](/en/ecosystem/skills/document-and-writing-skills/) and [documentation MCP servers such as Context7](/en/ecosystem/mcp/context7/).

## 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 `$ref`s | 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](/en/workflows/ship/evidence-bundle/).

## 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

- [Claude Code hooks: guardrails, gates, and live context](/en/claude-code/automate/hooks-automation/) covers every hook event and how to test hooks with fixtures.
- [Scripting Claude Code: headless runs, pipes, cron and Git hooks](/en/claude-code/automate/automation/) is the next step for turning the drift report into a scheduled job.
- [Using documentation as effective AI context](/en/workflows/context/documentation-as-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](/en/codex/build/documentation/) and [auto-generating docs with Cursor](/en/cursor-ide/build/documentation/).
