Stateful Agents and Basic Memory
One of the most limiting factors of LLM agents is they have limited cross-session memory. When working on software projects, this limitation isn’t much of an issue because the agent can search the project and read files to understand the current state and what it needs to do to make the changes you’ve requested. In a good software project, the code is a lot of the state you need to be successful with an agent. However, agents become a lot more interesting when you give them access to a persistent file system with instructions or scaffolding to load from that file system when a session begins.
While most implementations of memory in consumer products today are relatively opaque, it’s quite straightforward to add memory to your agent by giving it access to a file system.
From the perspective of an agent, memory is just a log of what happened.
You can write that record manually after each agent turn with something like a stop hook or you can add instructions to CLAUDE.md/AGENTS.md to tell the agent to write a record to a file after each conversation turn.
Once you know where you are writing these “memories”, prompt the agent that it can and should read from these memories as well.
There are many right ways to do this. Here is an extremely simple one.
# min-mem
After every conversation exchange, append a summary to `memories.jsonl` using a Bash tool call. Each line must be a JSON object matching this schema:
```json{ "timestamp": "ISO 8601 UTC", "summary": "1-3 sentence summary of what the user asked and what was done"}```
Use `jq -n -c` to build the JSON and `>>` to append. Example:
```bashjq -n -c --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg s "Summary here" '{timestamp: $ts, summary: $s}' >> memories.jsonl```
Before each conversation turn, search `memories.jsonl` for context relevant to the user's request. Use this to inform your responses with prior decisions, patterns, and context. Example:
```bashgrep -i "keyword" memories.jsonl | jq -r '.summary'```Here’s what interacting with this agent looks like. In this example, the agent uses tool calls to search for prior memories and save new ones:
This conversation turn results in the following memories.
{ "timestamp": "2026-02-18T21:23:20Z", "summary": "User introduced themselves as Jack."}From here, I closed Claude Code and opened a fresh session, then prompted the following.
And the memories are updated once more.
{"timestamp":"2026-02-18T21:23:20Z","summary":"User introduced themselves as Jack."}{"timestamp":"2026-02-18T21:23:56Z","summary":"User asked 'who am I?' — responded based on memories.jsonl that they are Jack."}Make it a Skill (optional)
If you like this approach, you can do a refactor of sorts to modularize your approach to memory for the agent using a Skill. Skills use a context management technique called progressive disclosure. This is a fancy way of saying the agent only sees the description of the skill by default and when to use it, but not the entire contents of the skill. Given the conversation, the agent decides whether to read the full contents of the skill (markdown and any other additional content). Otherwise, this is kept out of the context window to keep the agent focused on the task at hand.
It’s up to you whether or not you want to do this with memory.
If you always want the agent to read and write from memory after every conversation turn, there’s an argument to be made to leave the implementation in CLAUDE.md, but since most agents have project-specific content in there, let’s refactor to a skill.
I had to make some tweaks to the copy to get the Skill invocation working consistently and writing memories.jsonl to the root of the project.
ALWAYS start by using Skill("memory") before and after every conversation exchange.---name: memorydescription: Log and recall conversation context. Use after every conversation exchange to save a summary, and before responding to search for relevant prior context.allowed-tools: Bash(jq *), Bash(grep *), Bash(rg *), Bash(touch *), Read(memories.jsonl)---
# Memory Management
The memories file MUST be stored at the project root directory: `memories.jsonl`.
## After every conversation exchange
First, ensure the file exists, then append a summary:
```bashtouch memories.jsonl && jq -n -c --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg s "Summary here" '{timestamp: $ts, summary: $s}' >> memories.jsonl```
Each line must be a JSON object matching this schema:
```json{ "timestamp": "ISO 8601 UTC", "summary": "1-3 sentence summary of what the user asked and what was done"}```
## Before each conversation turn
Search `memories.jsonl` in the project root directory for context relevant to the user's request. Use this to inform responses with prior decisions, patterns, and context:
```bashgrep -i "keyword" memories.jsonl | jq -r '.summary'```
Search for multiple relevant keywords to find related context.Here’s what happens for the same interaction with the agent that has access to the memory Skill.
We get our memory written to the file.
{ "timestamp": "2026-02-18T19:49:33Z", "summary": "User introduced themselves as Jack. Greeted them back."}And now a fresh session.
And again we have two memory entries.
{"timestamp":"2026-02-18T19:49:33Z","summary":"User introduced themselves as Jack. Greeted them back."}{"timestamp":"2026-02-18T19:54:58Z","summary":"User asked 'who am I?' - recalled from memory that user is Jack."}Wrapping up
This is a pretty basic implementation, but it implements the most important part to give an LLM agent something that resembles memory - it reads and writes to a file system that persists beyond the agent’s sessions.
This approach allows the agent to catch up on what has already happened.
In our example, we allow the agent to decide what to read and write to and from memory.
You can be more opinionated about this if you want, using a framework like claude-agent-sdk where you have more control over the agent’s prompt and can do things like programmatically include the last N entries in the prompt.
For more on stateful agents, I recommend this post by Tim.