What is lessons.md and How Does Claude Code Self-Improve?
Problem
Claude kept making the same mistakes across sessions. I’d spend time debugging an issue, explain the fix to Claude, and then two sessions later, it would make the exact same mistake again.
For example:
Session 1: Claude tried to run migrations without checking for pending transactions. I explained: "Always check for pending transactions before ALTER TABLE" Claude acknowledged and fixed it.
Session 2: Same project, different feature. Claude tried to run migrations without checking for pending transactions. Same error. Same debugging. Same explanation.I had CLAUDE.md with rules and instructions, but it wasn’t enough. CLAUDE.md is great for defining how Claude should behave, but it doesn’t capture what Claude learned through actual mistakes.
What I discovered
I found a Reddit discussion about a pattern called lessons.md. The key insight was:
“lessons.md is a separate file from CLAUDE.md — it’s where the agent logs discoveries mid-task, not something you pre-populate.”
This was different from my approach. I had been writing all my knowledge into CLAUDE.md upfront. But lessons.md is meant to grow organically as Claude encounters issues.
CLAUDE.md -> Instructions you write BEFORE Claude workslessons.md -> Knowledge Claude writes AFTER making mistakesThe Reddit thread also highlighted a critical point:
“The real trick is making the write explicit in your instructions (‘before closing any task, update lessons.md’), because the model skips it when context gets heavy near the end of a session.”
This explained why my previous attempts at self-documenting failed. I didn’t explicitly instruct Claude to update the file before closing tasks.
How I set it up
I created a lessons.md file in my project root and added an explicit rule to CLAUDE.md:
## Session Hygiene
Before closing any task:1. Update lessons.md with any mistakes made2. Document patterns you discovered3. Note any gotchas specific to this codebase
Read lessons.md at the start of each session.Then I created an empty lessons.md:
# Lessons Learned
This file is updated by Claude as it discovers patterns and mistakes.Do not manually edit this file.I started with an empty file intentionally. The Reddit comment was clear:
“Pre-populating lessons.md defeats the purpose. It should grow from agent work, not your writing.”
What happened
I tested this setup on a real project with a PostgreSQL database. Here’s what lessons.md looked like after a week:
# Lessons Learned
## 2026-03-27: Database Migrations- Never run migrations in production without backup- Check for pending transactions before ALTER TABLE- The users table has a trigger that needs disabling during migrations- Always use --dry-run first with Django migrations
## 2026-03-25: API Response Format- Always include success boolean in responses- Error messages should be user-friendly, not stack traces- Rate limiting returns 429, not 500- The mobile app expects errors in {error: {code: string, message: string}}
## 2026-03-20: Frontend State- Don't mutate state directly in React components- Use immer for complex state updates- Debounce search inputs to 300ms- The search component has a race condition fix in useEffect cleanup
## 2026-03-18: Authentication Flow- JWT tokens are stored in httpOnly cookies, not localStorage- Refresh token endpoint is /auth/refresh, not /auth/token/refresh- Failed auth returns 401, not 403 (backend quirk)Notice how each entry came from a real mistake. The JWT token storage issue came from me wasting 30 minutes debugging why tokens weren’t in localStorage. Now Claude remembers this forever.
The workflow in action
Here’s how it works in practice:
Step 1: Session starts
Claude reads lessons.md automatically because of the rule in CLAUDE.md.
[Reading lessons.md from previous sessions]Found 4 dated entries covering:- Database migration patterns- API response format requirements- Frontend state management- Authentication flow quirksStep 2: Claude works
Claude applies the lessons during the session. For example, when I asked about implementing rate limiting:
Me: Add rate limiting to the user endpoint
Claude: I'll implement rate limiting. Based on lessons.md, I know that:- Rate limiting returns 429 (not 500)- Error format should be {error: {code: string, message: string}}
Implementing now...Step 3: New mistake happens
During implementation, Claude makes a mistake:
Claude: Running migration to add rate_limit column...
Me: Error! Migration failed - lock timeout
Claude: Let me check... Ah, there are pending transactions on users table. I should have checked this first.Step 4: Claude updates lessons.md
At the end of the session, Claude updates lessons.md:
## 2026-03-27: Rate Limiting Implementation- Always check for long-running queries before schema changes- Lock timeout on users table can be resolved with: SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE query LIKE '%users%'The key is the explicit instruction in CLAUDE.md. Without it, Claude skips this step when context gets heavy.
Why this matters
1. Persistent learning
Each session starts fresh, but lessons.md provides continuity. Mistakes that required debugging once get avoided in future sessions.
Without lessons.md: Session 1: Debug migration lock issue (30 min) Session 2: Debug same migration lock issue (30 min) Session 3: Debug same issue again (30 min)
With lessons.md: Session 1: Debug migration lock issue (30 min), write to lessons.md Session 2: Read lessons.md, avoid issue (0 min) Session 3: Read lessons.md, avoid issue (0 min)2. Project-specific knowledge
CLAUDE.md contains general rules. lessons.md captures quirks specific to YOUR codebase:
General rule (CLAUDE.md):- Check for pending transactions before migrations
Project-specific (lessons.md):- The users table has a trigger that needs disabling during migrations- JWT tokens are stored in httpOnly cookies, not localStorage- Failed auth returns 401, not 403 (backend quirk)3. Self-training without fine-tuning
You’re essentially training Claude on your specific project without any external tools or complex setups. The model literally trains itself on your codebase.
The open problem
The Reddit discussion raised a concern I’ve also noticed:
“If lessons.md grows forever, isn’t that just a second memory leak with better branding?”
This is a real problem. After a month, my lessons.md was 47KB. That’s context being consumed by potentially outdated information.
Week 1: 2KB - Quick winsWeek 2: 8KB - Still usefulWeek 3: 20KB - Some redundancyWeek 4: 47KB - Noise starts appearingThere’s no documented best practice yet for culling lessons.md. I’ve been manually reviewing it weekly:
1. Review lessons.md at end of week2. Remove entries that are now CLAUDE.md rules3. Remove entries that are no longer relevant4. Merge duplicate entries5. Keep only codebase-specific quirksThis is the open question: How do you keep useful bits without creating a junk drawer?
Common mistakes I made
Mistake 1: Pre-populating lessons.md myself
# Lessons Learned (my initial attempt)- Always use TypeScript strict mode- Never commit to main branch- Write tests before codeThis doesn’t work. These are general rules, not lessons from actual mistakes. They belong in CLAUDE.md, not lessons.md.
Mistake 2: Forgetting the explicit instruction
I initially just created lessons.md and hoped Claude would update it. It didn’t. The explicit rule in CLAUDE.md is critical:
Before closing any task:1. Update lessons.md with any mistakes madeMistake 3: Not checking if lessons.md is being used
I went two weeks before I realized Claude wasn’t reading lessons.md at session start. I added this verification step:
At the start of each session:- Read lessons.md- Summarize key learnings before starting workNow I see confirmation that lessons are being loaded:
[Session start]Reading lessons.md...Key learnings: 4 categories, 12 total lessonsReady to assist with your project.Summary
In this post, I showed how the lessons.md pattern creates persistent memory for Claude Code sessions. The key differences from CLAUDE.md:
- CLAUDE.md: You write instructions before Claude works
- lessons.md: Claude writes lessons after making mistakes
The setup is simple but requires explicit instruction:
- Create empty lessons.md in project root
- Add rule to CLAUDE.md: “Before closing any task, update lessons.md”
- Let it grow organically from real work
- Manually curate to prevent context bloat
The pattern works, but the “junk drawer” problem remains unsolved. There’s no automated way yet to determine which lessons are worth keeping and which should be pruned.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments