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)
- Copy these 2 files to your repo
- Tell Claude Code: "Read
.claude/RULES.mdbefore starting" - Run the consolidation script nightly
- 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:
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):
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:
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:
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.