Skip to content

Claude Code Self-Documenting System

Because every session of code starts with it forgetting what happened before, we rely on it to leave "breadcrumbs" around to find out things. But that process is very hit or miss. So I asked Claude to come up with a simple system that mostly works to improve its memory.

What follows is what it told me:


Quick Start (5 minutes)

  1. Copy these 2 files to your repo
  2. Tell Claude Code: "Read .claude/RULES.md before starting"
  3. Run the consolidation script nightly
  4. Done.

File 1: .claude/RULES.md

# Documentation Rules - READ THIS FIRST

## When you learn something non-obvious, add a comment:

```python
# LEARN: What you discovered

What counts as "non-obvious":

  • Behavior you had to test/experiment to find
  • Side effects (modifies global state, etc.)
  • Performance issues (slow with >1000 items, etc.)
  • Things that surprised you
  • "Gotchas" that wasted time

Examples:

✅ Good:

# LEARN: Must call initialize() first or this fails silently
# LEARN: Modifies global _registry dict as side effect
# LEARN: Throws ValueError not JSONDecodeError on bad input

❌ Don't bother:

# LEARN: This adds two numbers  (obvious from code)

That's it. No other tools needed.

The comments get consolidated into docstrings automatically.

**Save this as:** `.claude/RULES.md`

---

## File 2: `.claude/consolidate_learnings.py`

```python
#!/usr/bin/env python3
"""
Consolidates # LEARN: comments into docstrings.
Run nightly or before commits.
"""

import ast
import re
from pathlib import Path
from datetime import datetime


def extract_learn_comments(file_path):
    """Find all # LEARN: comments and their context."""
    with open(file_path) as f:
        lines = f.readlines()

    learnings = []
    current_function = None

    with open(file_path) as f:
        tree = ast.parse(f.read())

    # Map line numbers to functions
    function_map = {}
    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            for line_no in range(node.lineno, node.end_lineno + 1):
                function_map[line_no] = node.name

    for i, line in enumerate(lines, 1):
        if '# LEARN:' in line:
            func = function_map.get(i)
            if func:
                note = line.split('# LEARN:', 1)[1].strip()
                learnings.append({
                    'function': func,
                    'note': note,
                    'line': i
                })

    return learnings


def get_function_node(file_path, function_name):
    """Get AST node for a specific function."""
    with open(file_path) as f:
        tree = ast.parse(f.read())

    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef) and node.name == function_name:
            return node, tree
    return None, None


def update_docstring(file_path, function_name, new_notes):
    """Add notes to function docstring."""
    with open(file_path) as f:
        lines = f.readlines()

    func_node, _ = get_function_node(file_path, function_name)
    if not func_node:
        return False

    existing_doc = ast.get_docstring(func_node)
    func_line = func_node.lineno - 1

    # Build new docstring
    if existing_doc:
        # Add to existing
        doc_parts = existing_doc.split('\n')

        # Find or create "Learned:" section
        if 'Learned:' not in existing_doc:
            doc_parts.append('')
            doc_parts.append('Learned:')

        for note in new_notes:
            line = f'    - {note}'
            if line not in doc_parts:
                doc_parts.append(line)

        doc_parts.append('')
        doc_parts.append(f'Last Updated: {datetime.now().strftime("%Y-%m-%d")}')

        new_doc = '\n    '.join(doc_parts)
    else:
        # Create new docstring
        doc_parts = ['', 'Learned:']
        for note in new_notes:
            doc_parts.append(f'    - {note}')
        doc_parts.append('')
        doc_parts.append(f'Last Updated: {datetime.now().strftime("%Y-%m-%d")}')

        new_doc = '\n    '.join(doc_parts)

    # Find where to insert docstring
    insert_line = func_line + 1

    # Remove old docstring if exists
    if existing_doc:
        in_doc = False
        doc_start = None
        doc_end = None

        for i in range(func_line, min(func_line + 100, len(lines))):
            stripped = lines[i].strip()
            if not in_doc and (stripped.startswith('"""') or stripped.startswith("'''")):
                in_doc = True
                doc_start = i
                quote = '"""' if '"""' in stripped else "'''"
                if stripped.count(quote) >= 2:
                    doc_end = i
                    break
            elif in_doc and (quote in lines[i]):
                doc_end = i
                break

        if doc_start is not None and doc_end is not None:
            del lines[doc_start:doc_end + 1]
            insert_line = doc_start

    # Insert new docstring
    indent = '    '
    lines.insert(insert_line, f'{indent}"""{new_doc}\n{indent}"""\n')

    with open(file_path, 'w') as f:
        f.writelines(lines)

    return True


def remove_learn_comments(file_path, line_numbers):
    """Remove processed # LEARN: comments."""
    with open(file_path) as f:
        lines = f.readlines()

    # Remove from highest line number to lowest to preserve indices
    for line_no in sorted(line_numbers, reverse=True):
        if '# LEARN:' in lines[line_no - 1]:
            del lines[line_no - 1]

    with open(file_path, 'w') as f:
        f.writelines(lines)


def consolidate_file(file_path):
    """Process one Python file."""
    learnings = extract_learn_comments(file_path)
    if not learnings:
        return 0

    # Group by function
    by_function = {}
    for item in learnings:
        func = item['function']
        if func not in by_function:
            by_function[func] = []
        by_function[func].append(item['note'])

    # Update docstrings
    for func_name, notes in by_function.items():
        update_docstring(file_path, func_name, notes)

    # Remove processed comments
    line_numbers = [item['line'] for item in learnings]
    remove_learn_comments(file_path, line_numbers)

    return len(learnings)


def main():
    """Consolidate all LEARN comments in the repo."""
    total = 0

    for py_file in Path('.').rglob('*.py'):
        # Skip virtual envs and hidden dirs
        if any(part.startswith('.') or part == 'venv' for part in py_file.parts):
            continue

        try:
            count = consolidate_file(py_file)
            if count > 0:
                print(f"✓ {py_file}: {count} learnings consolidated")
                total += count
        except Exception as e:
            print(f"✗ {py_file}: {e}")

    print(f"\nTotal: {total} learnings consolidated")
    return 0


if __name__ == '__main__':
    exit(main())

Save this as: .claude/consolidate_learnings.py
Make it executable: chmod +x .claude/consolidate_learnings.py


How to Use

During Development (Claude Code does this):

Just add comments when you learn something:

def parse_config(filename):
    # LEARN: Throws ValueError not JSONDecodeError on malformed JSON
    # LEARN: Caches results in _cache dict for 5 minutes
    with open(filename) as f:
        return json.load(f)

Daily/Weekly (You run this):

python .claude/consolidate_learnings.py

This will: 1. Find all # LEARN: comments 2. Add them to the function's docstring under "Learned:" section 3. Remove the comments 4. Update timestamps

Before Each Claude Code Session:

Tell it: "Read .claude/RULES.md and follow the documentation rules"


What You Get

Before:

def parse_config(filename):
    with open(filename) as f:
        return json.load(f)

After Claude Code works on it:

def parse_config(filename):
    # LEARN: Throws ValueError not JSONDecodeError on malformed JSON
    # LEARN: Caches results in _cache dict for 5 minutes
    with open(filename) as f:
        return json.load(f)

After running consolidation:

def parse_config(filename):
    """

    Learned:
        - Throws ValueError not JSONDecodeError on malformed JSON
        - Caches results in _cache dict for 5 minutes

    Last Updated: 2025-10-24
    """
    with open(filename) as f:
        return json.load(f)


Optional Enhancements

Auto-run on commit:

Add to .git/hooks/pre-commit:

#!/bin/bash
python .claude/consolidate_learnings.py
git add -u  # Add updated docstrings

Auto-run nightly:

Add to crontab:

0 2 * * * cd /path/to/repo && python .claude/consolidate_learnings.py && git commit -am "docs: consolidated learnings" && git push

Team review:

Instead of auto-committing, create a PR:

python .claude/consolidate_learnings.py
git checkout -b docs/learnings-$(date +%Y%m%d)
git commit -am "docs: consolidated learnings"
git push origin HEAD
# Open PR for team review


Why This Works

No memory needed: Claude Code just adds comments
Simple pattern: One comment type, easy to remember
Automatic: Consolidation script does the work
Reviewable: Can review before committing
Progressive: Works even if Claude forgets sometimes
Zero dependencies: Pure Python stdlib


Troubleshooting

Q: Claude Code isn't adding comments
A: Remind it each session: "Follow .claude/RULES.md documentation rules"

Q: Script fails on syntax error
A: It skips broken files and continues. Fix syntax, re-run.

Q: Duplicate learnings in docstring
A: Script checks for duplicates, won't add twice.

Q: Want different comment syntax?
A: Change # LEARN: to whatever in both files (e.g., # CLAUDE:)


Summary

Setup: 2 files, 5 minutes
Usage: Add # LEARN: comments
Maintenance: Run script daily/weekly
Result: Self-documenting codebase that captures tribal knowledge

That's it. 20% effort, 80% benefit.