Documentation has a specific failure mode: written once, stale within weeks, actively misleading within months. Claude Code doesn't fix the cultural problem — it fixes the effort problem.
Start with an Audit
Before generating anything, get a picture of the current state:
Read through src/ and tell me:
1. Which exported functions have zero JSDoc/TSDoc
2. Which have outdated JSDoc (types that don't match the implementation)
3. Whether README.md accurately describes the current entry points
4. What's in the codebase that isn't mentioned in any documentation
Output as a plain list, one issue per line.
JSDoc/TSDoc Generation
Take any undocumented function:
export async function createInvite(
workspaceId: string,
email: string,
role: 'admin' | 'member' | 'viewer',
expiresInDays?: number
): Promise<{ token: string; expiresAt: Date; link: string }> {
// ...
}
Prompt:
Read src/lib/invites.ts and add TSDoc to every exported function.
Format: summary → @param → @returns → @throws → @example
Do not change any implementation code. Only add the JSDoc blocks.
Claude's output:
/**
* Creates a pending workspace invitation and returns a tokenized invite link.
*
* @param workspaceId - The workspace to invite the user into
* @param email - Email address of the person being invited
* @param role - Permission level granted when the invite is accepted
* @param expiresInDays - Days until the invite expires (default: 7)
* @returns Object containing the token, expiry date, and full invite URL
* @throws {ConflictError} If a pending invite already exists for this email
*
* @example
* const invite = await createInvite('ws_abc123', 'alice@example.com', 'member')
* // Send invite.link via email
*/
export async function createInvite(/* ... same implementation */)
Batch JSDoc for an entire directory
for file in src/lib/*.ts; do
echo "Documenting $file..."
cat "$file" | claude -p \
"Add TSDoc to every exported function and type. Do not change implementation.
Output the complete file with documentation added." \
--output-format text > "$file.tmp" && mv "$file.tmp" "$file"
done
Always review after — Claude occasionally misinterprets ambiguous parameter names.
README Generation
Read the entire project — package.json, src/, .env.example, and any existing README.md.
Generate a README.md with:
1. What this project does (from the code's actual behavior)
2. Prerequisites (Node version, required env vars from .env.example)
3. Setup (exact commands)
4. Running locally (from package.json scripts)
5. Project structure (top-level only)
6. Environment variables (table: name, required/optional, description, example)
7. Deployment (only if Dockerfile or CI config exists)
Be accurate. If unsure about something, say so rather than guessing.
Key instruction: "accurate, not complete." A README that says "I'm not sure about the deployment step" is better than one that confidently describes the wrong process.
Pre-push hook: README staleness check
#!/bin/bash
# .git/hooks/pre-push
CHANGED=$(git diff --name-only origin/main...HEAD -- src/)
if [ -n "$CHANGED" ]; then
NEEDS_UPDATE=$(echo "$CHANGED" | claude -p \
"These src files changed: $CHANGED
Read README.md. Does it reference anything that might now be inaccurate?
Answer: YES or NO and one sentence." \
--output-format text)
if echo "$NEEDS_UPDATE" | grep -q "^YES"; then
echo "⚠️ README may need updating: $NEEDS_UPDATE"
exit 1
fi
fi
This asks you to verify, not auto-update. Auto-updating docs without review produces confidently wrong docs.
Architectural Decision Records (ADRs)
ADRs document why a decision was made. Most valuable, least likely to be written.
We just decided: Use Drizzle ORM instead of Prisma for all new queries.
Write an ADR in this format:
# ADR-[number]: [Short title]
Date: [today]
Status: Accepted
## Context
## Decision
## Consequences (Positive / Negative / Risks)
## Alternatives Considered
Generate a draft from git history:
DIFF=$(git show "abc123")
claude -p "This commit made significant architectural changes:
$DIFF
Write a draft ADR explaining the problem it solves, decision made, and trade-offs." \
--output-format text > docs/decisions/draft-adr-$(date +%Y%m%d).md
PostToolUse Hook: Remind on Implementation Changes
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node -e \"const f = process.env.TOOL_INPUT_FILE || ''; if (f.includes('/src/') && !f.includes('.test.') && !f.includes('.md')) { console.log('📝 Check if JSDoc and README need updating for ' + f); }\""
}
]
}
]
}
}
Every time Claude edits a source file, this reminder appears in context.
CLAUDE.md Documentation Rules
## Documentation rules
### When adding or changing a function:
- Add/update TSDoc on every exported function you touch
- Format: summary → @param → @returns → @throws (if applicable) → @example
- Skip private/internal functions unless non-obvious
### When the change affects:
- Environment variables → update .env.example and README env table
- CLI commands → update README setup section
- New dependencies → add to README prerequisites if they need setup
### Don't document:
- Implementation details obvious from the code
- Internal state variables
- Test setup functions
Keeping Docs Current
Periodically run:
Read docs/ and compare against the current src/.
List documentation that references code, APIs, or behaviors
that no longer exist or have changed significantly.
This surfaces outdated docs faster than a human review.
The rule of minimum viable docs: only document what changes frequently or isn't obvious from the code. Over-documented code creates maintenance overhead without proportional value.
Full article at stacknotice.com/blog/claude-code-documentation-2026
Top comments (0)