Memory Systems: Teaching Claude to Remember
Every conversation with Claude starts from zero. New session, blank slate. It knows nothing about your projects, your preferences, your codebase, or the conversation you had with it twenty minutes ago. The context window is generous but ephemeral. Session ends, everything evaporates. Next session starts as a stranger.
For one-off questions, that is fine. For using Claude as your primary development tool across multiple projects, days, and contexts, it is not. You do not want to re-explain your database schema every morning. You do not want to re-state your coding conventions every session. You do not want to re-describe the architecture of a system that Claude helped you build last week.
We needed persistent memory. Not the kind baked into model weights, but operational memory. What are we working on. What decisions have been made. What the user prefers. What went wrong last time. The kind of memory that turns a capable tool into something closer to a colleague.
// the approach
The obvious solution is to dump everything into the context window at session start. One giant text file of notes, project state, preferences, recent history. Let Claude sort through it. This works until the accumulated context exceeds what you can fit in a prompt without degrading quality. It also treats all memories as equally important, which they are not.
Our approach is file-based memory with intelligent retrieval. Memories live as individual markdown files in a structured directory. Each has a type, title, and content. A new session does not load everything. It loads what is relevant, scored by recency, frequency of access, and semantic relevance to the current context.
The storage layer is deliberately simple. Markdown files in directories. No database for the memories themselves. No binary format, no proprietary encoding. You can open the memory directory in a text editor and read every memory the system has. This is intentional. A memory system you cannot inspect is a memory system you cannot trust. Trust matters when the memory is influencing decisions about your codebase.
On top of the files sits an FTS5 full-text search index. FTS5 is SQLite's built-in full-text search, and it gives us fast ranked retrieval across the entire memory corpus without an external search service. Query the index with relevant terms, get back a ranked list of memory files sorted by relevance and weighted by salience scores.
Simple storage, intelligent retrieval. Memories are human-readable and debuggable. Retrieval scales to thousands of entries without slowing down. You can always fall back to reading the files manually, but the search is good enough that you rarely need to.
// salience scoring
Not all memories are equal. A note about a critical production bug fixed yesterday matters more than a library version bump from three months ago. A preference that applies to every session outweighs a sprint-specific status update. The system needs to understand this hierarchy.
Every memory gets a salience score computed from three signals. Recency tracks when the memory was created or last accessed. Newer memories score higher. The decay is exponential but gentle - last week's memories still score well, six months ago scores significantly lower.
Frequency tracks how often the memory has been accessed. A memory retrieved in every other session is clearly important to ongoing work. Frequency separates one-off notes from things that keep being useful.
Relevance is computed at query time from the FTS5 ranking against search terms. The classic information retrieval signal: how well does this memory's content match what we are looking for right now? Combined with recency and frequency, it ensures results are not just textually similar but also temporally appropriate.
The three signals combine with configurable weights. Relevance dominates by default, recency breaks ties, frequency boosts consistently accessed memories. The weights are tuneable per query. A broad context refresh can weight recency higher. A targeted lookup can weight relevance higher.
In practice: start a CoachSync session and the system retrieves CoachSync project status, recent feedback, relevant technical references. It does not also pull in memories about unrelated projects or last month's Telegram bot configuration. Focused context, not a firehose.
// the hook system
Memories are only useful if the system actually creates them. Relying on manual memory creation is like relying on manual backups. It works right up until you forget, which is immediately.
Hooks automate memory creation. They fire on system events. A CLAUDE.md file gets created or modified - a hook indexes its contents. A session generates feedback (a correction, a preference, a "do not do that again") - a hook creates a feedback memory. A project status changes - a hook updates the project memory.
Each hook is a function watching for a specific event pattern. When triggered, it creates or updates a memory file and re-indexes it in FTS5. Hooks run synchronously, so there is no eventual consistency delay. Tell Claude "always use single dashes, never em-dashes" and the feedback hook captures it, indexes it, and makes it available to the next query before the current response finishes.
Hooks also handle maintenance. A cleanup hook scans for memories with months of inactivity and low salience, flagging them for archival. An update hook detects stale content - a project status for something that shipped, say - and marks it accordingly. The memory corpus stays clean without manual gardening.
The most valuable hooks are the ones you forget exist. The feedback hook has quietly accumulated hundreds of preferences, corrections, and conventions over months. Every time you correct Claude, that correction becomes permanent. Over time it builds a detailed profile of how you want things done, and new sessions inherit all of it.
// the thinking loop
Raw memories are data. They are not insight. Knowing you fixed a bug yesterday, deployed a feature this morning, and have a meeting tomorrow is useful. Synthesising that into "the release is on track, the blocking bug is resolved, stakeholder sync at 9" is more useful. The thinking loop bridges that gap.
It runs as an hourly cron job. Reads recent memories, project statuses, calendar events, email summaries. Generates a structured briefing capturing the current state of everything. The briefing gets loaded at session start, giving Claude an immediate synthesised picture without having to piece it together from raw memories.
The briefing covers project statuses (in progress, blocked, recently shipped), upcoming commitments (meetings, deadlines, scheduled tasks), recent activity (emails handled, code deployed, issues resolved), and flagged items (things needing attention, anomalies, overdue tasks). The kind of summary a human PA would prepare for a morning standup, except it runs hourly and is always current.
The loop also connects things. A memory about a Stripe webhook failure and a memory about a customer complaint might not reference each other, but the thinking loop can spot the correlation and flag it in the briefing. That cross-referencing turns passive storage into something more like active intelligence.
Morning and midnight briefings run on a separate, more comprehensive schedule. Morning focuses on overnight events and the day ahead. Midnight summarises the day and flags anything needing attention before tomorrow. Both are delivered via email and the Telegram bot, so the daily rhythm is captured even without an active Claude session running.
// memory types
Memories are organised by type, reflecting their purpose and lifecycle. Each type has different default salience weights and retention policies.
memory/
├── MEMORY.md # Master index with categorised links
├── briefing.md # Latest thinking loop output
├── engine/ # FTS5 index and scoring logic
│ ├── index.db # SQLite FTS5 database
│ ├── hooks/ # Event-driven indexing hooks
│ └── scoring.ts # Salience calculation
├── user_*.md # User preferences and personal info
├── feedback_*.md # Corrections, conventions, do/don't
├── project_*.md # Project statuses and decisions
└── reference_*.md # Technical references and docs
User memories cover personal information and preferences. Email accounts, tool configs, workflow habits, personal interests. High base salience because they apply everywhere and rarely go stale. A timezone preference is relevant in every session, indefinitely.
Feedback memories capture corrections and conventions. "Always lowercase for nullhex." "Never use em-dashes." "Register migrations after applying via Management API." The accumulated wisdom of past mistakes and stated preferences. Highest retrieval priority because they directly influence how Claude behaves right now.
Project memories track active work. What has been built, what is in progress, what decisions were made and why. Moderate salience that decays as projects complete. Current work surfaces prominently, finished work fades into the archive.
Reference memories hold technical documentation, deployment procedures, API details, config notes. The factual foundation other memories build on. Stable salience over time - technical facts do not expire with age, though they can become outdated, which the maintenance hooks watch for.
The type system is a naming convention, not a rigid schema. A file named feedback_email_mark_read.md automatically gets feedback-type salience weights, but you can override them. The system adapts to how memories are actually used rather than forcing them into predetermined boxes.
// the foundation
Memory is not a feature. It is the foundation on which every other capability is built. Without memory, every session starts from ignorance. With memory, every session starts from understanding.
Before the memory engine, every session needed a preamble. "We are working on CoachSync, it is a coaching platform, here is the stack, here are the conventions, last time we were doing the booking flow." That consumed context window space and human patience in roughly equal measure.
Now sessions start informed. Claude knows the projects, the preferences, the recent history. It knows nullhex is always lowercase. It knows CoachSync uses a proxy, not middleware. It knows emails should be marked as read after handling. Every correction we have ever made is baked into the starting context.
It is not perfect. Salience scoring occasionally surfaces something irrelevant or misses something useful. The thinking loop sometimes generates a verbose briefing or focuses on the wrong priorities. Hooks occasionally miss an event. But these are tuning problems, not architectural ones. The foundation is solid and the rough edges are getting smoother with each iteration.
We built this because AI assistants should get better over time, not start fresh every conversation. The knowledge from hundreds of sessions is too valuable to throw away. Memory is what turns a tool into a collaborator.