Anthropic's components of agent memory in Claude Code: audit MEMORY.md, keep the transcripts, run your own dreaming pass
Claude Code keeps the notes and the startup index indefinitely, sweeps the transcripts behind them after 30 days by default, and ships no pass that checks one against the other. Anthropic's results show why that check is worth having, so here is a way to run it locally and read the result before any of it lands.
Contents
Anthropic’s engineering blog published a long post on 6 November, “The components of agent memory”. It defines memory as context Claude writes from experience and reads in a later session (weights, in-context learning and compaction are out of scope), splits it into four layers, and runs experiments on several of them. Read next to the Claude Code documentation, it exposes a mismatch in the defaults: Claude Code keeps the notes until someone deletes them and sweeps the transcripts behind them after 30 days, while the post’s main repair mechanism works by checking one against the other.
The numbers first. On the database task from CL-Bench 1.0, memory lifts Opus 5.5 from 0.655 to 0.838 mean reward and Opus 4.7 from 0.383 to 0.700. Sonnet 4.6 barely moves, 0.330 to 0.364, until it is told to read its memory and test its theories, and then it reaches 0.641. The gain came from reading and testing what was stored, not from storing more.
What follows is the audit I would run on a Claude Code setup after reading the post, ending with the consolidation pass Anthropic calls dreaming, run on a copy, with a diff at the end.
What are the four layers in Anthropic’s components of agent memory?
Transcripts, memory notes, memory representations and injected context, from the bottom up. Transcripts are the searchable event log of past sessions. Notes are what Claude chose to write down, such as files in a memory directory. Representations are structured forms Claude maintains on top, a linked wiki being the main example. Injected context is what loads into every session before the first prompt.
Dreaming is the pass that moves information between them. In Anthropic’s description it tests notes against the original transcripts, rewrites the ones that turn out wrong, builds and prunes representations, and can promote a note into the injected layer. The post’s clearest example comes from a Pokémon Red run: a wrong note about where the Pokémon Center was led Claude off a one-way ledge in Mt. Moon three times, and after a dreaming pass checked the note against the session transcript and rewrote it, Claude got through the cave.
The wiki results are the ones I would read twice, because they cut both ways. On the Stainless corpus, about 350,000 files, the wiki alone climbed from 13% to 48% of graded criteria as dreaming ran over more sessions and still lost to plain search at 53%; wiki plus search reached 61% and saved about 20,000 tokens per question. On Harvey’s LAB merger questions, which needed synthesis across documents, the wiki won outright, 87% against 73% for search. On another LAB task the wiki grew to 1.77 times the size of its source, 203 KB against 115 KB, and the post says it “effectively abstracted nothing”. A representation layer helps where the answer has to be assembled, and it is not compression by default. It compresses only if something keeps it that way.
Where does each memory layer live in Claude Code?
Two of the four layers are built in, one is built in but expires, and one does not exist unless you build it. On Claude Code 2.1.319:
Layer Claude Code Lifetime
transcripts ~/.claude/projects/<project>/*.jsonl swept once older than
(subagents under <session>/subagents/) cleanupPeriodDays (default 30)
memory notes ~/.claude/projects/<project>/memory/*.md kept until edited or deleted
representations none built in
injected context CLAUDE.md files + first 200 lines or 25KB of every session
memory/MEMORY.mdTopic files are not loaded at startup. Claude reads one when it decides it needs it, mostly on the strength of its one-line entry in MEMORY.md. The index is the only part of the notes layer guaranteed to be in the window, which is the same map problem I described in the Markdown memory essay: past the cutoff, a note is found only by an agent that already thought to look.
Dreaming itself is not in Claude Code. Anthropic ships it as dreams in Claude Managed Agents, a research preview behind an access request and the dreaming-2026-04-21 beta header. A dream takes a memory store and 1 to 100 session transcripts and writes a new store, with duplicates merged and contradicted entries replaced. The input store is never modified, so you review the output and attach it or throw it away. That design choice is worth copying even where the API is not available.
How do you check MEMORY.md against Claude Code’s 200-line and 25KB cutoff?
Measure every index, and look for two things the size limit does not show: index lines that point at a file that no longer exists, and topic files the index never names. This script does all three across every project:
#!/usr/bin/env bash
# Audit Claude Code auto memory indexes: size against the 200-line / 25KB load,
# index lines pointing at missing files, topic files the index never names.
# Raw-file numbers: Claude Code measures the index after stripping frontmatter
# and HTML comments, so treat this as an approximation.
# Usage: memory_audit.sh [projects_dir] (default ~/.claude/projects)
ROOT="${1:-$HOME/.claude/projects}"
for idx in "$ROOT"/*/memory/MEMORY.md; do
[ -f "$idx" ] || continue
dir=$(dirname "$idx")
lines=$(awk 'END { print NR }' "$idx") # counts a last line with no newline
bytes=$(wc -c < "$idx")
status=ok # 25KB taken as 25,600 bytes
if [ "$lines" -gt 200 ] || [ "$bytes" -gt 25600 ]; then status=CUT
elif [ "$lines" -gt 160 ] || [ "$bytes" -gt 20480 ]; then status=near # my 80% mark
fi
links=$(grep -oE '\]\([^)]+\.md\)' "$idx" | sed -E 's/^\]\(//; s/\)$//' | sort -u)
dangling=0
while IFS= read -r f; do
[ -n "$f" ] && [ ! -f "$dir/$f" ] && dangling=$((dangling+1))
done <<< "$links"
orphans=0 # whole file name, linked or not
for f in "$dir"/*.md; do
b=$(basename "$f"); [ "$b" = MEMORY.md ] && continue
grep -qE "(^|[^[:alnum:]_.-])${b//./\\.}([^[:alnum:]_.-]|\$)" "$idx" || orphans=$((orphans+1))
done
printf '%-5s %4d lines %6d bytes dangling=%d orphans=%d %s\n' \
"$status" "$lines" "$bytes" "$dangling" "$orphans" "$(basename "$(dirname "$dir")")"
done | sort -k2 -nrThe top of the output on my machine, with project names replaced:
near 184 lines 10393 bytes dangling=0 orphans=0 project-a
ok 110 lines 9578 bytes dangling=0 orphans=2 project-b
ok 90 lines 10204 bytes dangling=0 orphans=3 project-cNone of my indexes is past the cutoff. On my largest one the line limit is the one that binds: 25KB over 200 lines is 128 bytes per line, and that index runs about 56, so it would reach 200 lines at around 11KB. The byte limit starts to matter when entries get long, which usually means they hold content that belongs in a topic file. The near mark is my own 80% threshold, not whatever Claude Code uses for its reminder.
The orphans column is the one I would look at first. Claude Code already watches the size: after Claude writes MEMORY.md, it reminds Claude to shorten an index that is near a limit and returns an error for one over it. Nothing watches a topic file that dropped out of the index, through a manual edit or a consolidation that merged its line away. The file is still on disk and Claude can still find it by listing or searching the folder, but the startup index no longer advertises it, so a session that does not go looking will not see it. For each orphan, either add its line back to MEMORY.md or delete the file. A dangling line is the reverse case and should simply go.
The orphan check took me two wrong versions. Matching the file name as a plain substring misses a real orphan whose name is the tail of another file’s name, so note.md hides behind other_note.md. Matching it only as a link target flags a note the index names in backticks, which Claude reads fine; on my own folders that version reported a false orphan. Matching the name as a whole token avoids both.
The audit turned up one more thing. Claude Code records a modified timestamp in a memory file’s frontmatter when Claude writes it, and on my machine few topic files carry one. The file’s mtime is the fallback, and it is weak evidence: it says when the file last changed, not when its content was last true, and a copy or restore can reset it. Use it to decide which notes to re-check first, not to decide which are stale: ls -lt --time-style=+%F ~/..
How long does Claude Code keep the transcripts a dreaming pass needs?
Thirty days by default, and the deletion is silent. cleanupPeriodDays sets the retention for session transcripts; the default is 30 and the minimum is 1, and a background sweep after a session starts deletes transcript files older than that without a message. The memory directory is explicitly excluded from the sweep. Notes stay until someone edits or deletes them.
Put that next to the post. Dreaming fixes a note by testing it against the transcripts, and Anthropic’s open questions admit notes can be “wrong from the start”. A note outlives the session it came from by default, so once that transcript is old enough to be swept, the evidence that could show the note was wrong is gone and the note is not. The longer a note lives, the less evidence is left to check it against.
My own settings keep transcripts for 36500 days, which is effectively forever. If you want any kind of consolidation pass, raise the value in ~/.claude/settings.json well past the age of the notes you expect to re-check:
{
"cleanupPeriodDays": 365
}Two costs come with it. Transcripts accumulate on an active project, and they hold everything that passed through a session: tool results, file contents, any secret that was printed. A pass that reads them should copy only a recent window and delete its copy afterwards, and the script below does both.
What should Claude Code memory instructions say: what memory is for, or rules for using it?
What it is for. Anthropic’s sentence is direct: “We’ve found that it helps to tell Claude what memory is for, but not how to use it.” When the team added a fixed rule, “record only failures”, Opus 5 lost 0.234 of its memory gain and Opus 4.7 lost 0.150. Sonnet 4.6 lost only 0.020, having little gain to lose; what helped it was a prompt to read its memory and test its theories, which nearly doubled its score.
Claude Code’s built-in auto memory instructions mix the two: a purpose (save what would be useful in a future conversation) next to rules (four note types, skip anything derivable from the code, skip what CLAUDE.md already says). Adding to CLAUDE.md does not remove those defaults, so the lever you have is what you add, and the evidence points at a sentence about purpose rather than another format rule. Something like this in a project’s CLAUDE.md:
## Memory
Memory exists so a future session repeats nothing I already corrected and
keeps what was tested here. Before acting on a memory note, check it against
the code or a command; if they disagree, trust the check and fix the note.The CL-Bench results come from a database task, not a Claude Code session, so treat the transfer as a reasonable bet and not a measured result. The second sentence is the cheap half of dreaming: a note gets tested at the moment it is used, which is the moment a wrong note costs something. It is still a sentence the model may skip, which is the limit of every rule in that file. The pass below does not depend on the agent remembering to check.
How do you run a dreaming pass on Claude Code memory yourself?
Put the memory folder under git first, so an accepted change is a commit and a bad one is a revert. Then copy the folder and a recent window of transcripts to a scratch directory, let a headless claude -p check the notes against the transcripts and rewrite only the copy, and read the diff. That mirrors the property of Anthropic’s dreams that matters most here: the input is never modified.
#!/usr/bin/env bash
# Local dream pass: check a copy of Claude Code auto memory against recent transcripts.
# Usage: dream.sh <full path to ~/.claude/projects/<project>> [days, default 14]
set -euo pipefail
[ $# -ge 1 ] || { echo "usage: dream.sh <project dir> [days]" >&2; exit 64; }
PROJ=$(realpath -e -- "$1"); DAYS="${2:-14}"
[[ "$DAYS" =~ ^[1-9][0-9]*$ ]] || { echo "days must be a positive integer" >&2; exit 64; }
[ -d "$PROJ/memory" ] || { echo "no memory/ in $PROJ" >&2; exit 66; }
WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT # transcripts never outlive the run
OUT="${OUT:-$PWD/dream-$(date +%F)}"; mkdir -p "$OUT"
cp -r "$PROJ/memory" "$WORK/memory"
# top-level transcript files modified in the last $DAYS days
find "$PROJ" -maxdepth 1 -type f -name '*.jsonl' -mtime -"$DAYS" -exec cp -t "$WORK" -- {} +
ls "$WORK"/*.jsonl > /dev/null 2>&1 || { echo "no transcripts in the window" >&2; exit 65; }
cd "$WORK"
CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 claude -p "$(cat <<'PROMPT'
./memory is an agent's memory. The *.jsonl files here are recent session transcripts.
Memory exists so a future session repeats nothing the user already corrected and keeps what was tested.
Check every note in ./memory against the transcripts. Where a transcript contradicts a note,
rewrite the note to match the latest evidence. Where a correction or tested rule appears in the
transcripts and no note holds it, add one and index it in ./memory/MEMORY.md. Leave notes the
transcripts do not mention unchanged. Edit only files under ./memory. Then write ./CHANGES.md:
one line per change, with the transcript file and the line that justifies it.
PROMPT
)" --restricted --tools "Read,Grep,Glob,Edit,Write" --permission-mode acceptEdits \
--no-session-persistence < /dev/null > "$OUT/run.log" 2>&1
rc=0; diff -ruN "$PROJ/memory" "$WORK/memory" > "$OUT/memory.diff" || rc=$?
[ "$rc" -le 1 ] || exit "$rc"
cp -r "$WORK/memory" "$OUT/memory"; cp "$WORK/CHANGES.md" "$OUT/" 2>/dev/null || true
echo "review: $OUT/memory.diff $OUT/CHANGES.md (proposed files in $OUT/memory)"The flags carry the safety, not the prompt. --restricted confines the file tools to the working directory, which is the scratch copy, and --tools leaves no tool that runs commands. --no-session-persistence keeps the pass from saving a transcript of its own, which would otherwise be a fresh copy of everything it just read, under a new project folder, kept for as long as your retention allows. CLAUDE_CODE_DISABLE_AUTO_MEMORY= stops it writing memories of its own. The trap deletes the copied transcripts when the script exits, and the diff, the proposed files and CHANGES.md stay in $OUT for review. Two limits to know: the window is files modified in the last N days, so a long session resumed yesterday comes in whole, and subagent transcripts in each session folder are left out. The pass is billed like any other run and scales with how much transcript it reads, so start with a few days.
I ran it on Claude Code 2.1.319 against a synthetic setup, not my real memory: two notes (tests run with npm test and need a local Redis; staging deploys from a staging branch) and a four-line transcript, dated 1 November, in which the user says the project moved to pnpm last week and Redis is gone, and the agent reports 214 tests passing. Across three runs, the pass changed the test note every time and left the deploy note alone, since no transcript mentioned it. Two of the runs, on an earlier version of the script, also added a date. This is the second of them:
-Run tests with `npm test`; the suite needs a local Redis on port 6379.
+Run tests with `pnpm test` (project moved from npm to pnpm in late Oct 2026; `npm test` is outdated).
+No Redis needed: tests use an in-memory fake. Verified 2026-11-01: 214 tests passed.That is the result you want, and one detail in it is why you read the diff instead of copying the folder back blind. “Late Oct 2026” is not in the transcript. The user said “last week”, and the pass turned that into a date of its own; the date and the test count on the next line are both in the transcript, the month is not. It is a plausible inference and also a new, unsourced claim in a file that will be treated as fact for months. CHANGES.md cited line 3 for it, and line 3 does not contain it. The third run wrote the note without the date. Review, edit, copy the files you accept back by hand, and commit.
What does a dreaming pass not fix in agent memory?
A transcript-only pass cannot see a change no session witnessed. It corrects a note the log contradicts, and the Mt. Moon note was wrong in a way the same run exposed. A dependency that moved in a teammate’s commit, or a decision made in a meeting, leaves no line in any transcript, and the pass keeps the old note exactly as confidently as before. When I wrote that a readable memory is still a memory, the missing piece was a signal from outside the sessions when the world moves. Checking against the log does not supply one; Anthropic’s wiki pipeline checks claims against source documents as well, and a local pass could read the repository or its git log too. What the transcript check does change is who runs it: a separate step instead of the agent mid-task. Anthropic is candid that it costs compute and that checking every memory continuously may not be practical.
What dreaming gets right is the shape of the write. The input store is untouched and the output is a separate store you choose to adopt, which is the versioned record I argued a document memory needs. Locally, that is the git commit before the pass. The same post supplies evidence for the other point from that essay, about who may write which page: agents writing into one shared table of merger terms kept 41 of 57 facts, and one page per contract, with terms quoted directly, kept all 57.
The representation layer deserves the same suspicion as the notes. Weights avoid the problem by having no file to grow at all, and pay for it in other ways. A wiki that grows past its source has not abstracted anything, however well it scored on the questions it was built for. If you build one for Claude Code, keep search next to it and measure it the way you measure the index, by what it costs to load and whether anything in it is never read.
If your MEMORY.md is approaching 200 lines, trimming is the obvious move, and it answers half the question. It keeps the index under the cutoff. It does not tell you which entries are still true. That second question needs the evidence, and in a default setup the evidence is swept after a month. Raise cleanupPeriodDays before the next sweep, then trim with the transcripts still there to check against.
01 / Discussion
No comment section here — all discussions happen on X.
02 / More articles
Vibe coding vs agentic engineering: what still checks the code after the session ends
Simon Willison draws the line at a person who reviewed the code and can explain it. That review happens once, and the code keeps changing after the chat closes. If agentic engineering is a different thing and not a nicer name, the difference is a check that runs without anyone in the room, and that has been seen to fail for the right reason.
Google's SLEEP framework vs agent memory files: weights drop the context tax and the address with it
Consolidating memory into weights is a real answer to the cost of rereading notes on every call. It also takes away what every operator action on memory needs, something to point at. In SLEEP not even last night is a block you can lift out, so the unit of rollback is a checkpoint and the cost of forgetting grows with the age of what you forget. Weight memory is safe to run only as a cache rebuilt from a record you kept.
MCP Tasks and elicitation in Claude Code: a question nobody may be there to answer
Claude Code now hands a long job to an MCP server and checks back on it. Once it does, the client stops owning that job: no time limit after backgrounding, no cancel on exit, and a question from a running task that only a person at a terminal can answer. Deadlines and cleanup land on the server author.