Guides

Why Claude Code Forgets Between Sessions, and How Ours Remembers

Claude Code forgets context between sessions because each session starts with an empty context window. Cross-session memory is a routine that makes an AI coding agent look up past decisions before work and save new ones after, so a fresh context window starts informed.

David Marsa

David Marsa

Founder & CEO

Beginner10 min readPublished Oct 26, 2026

Last verified Oct 26, 2026

Tools and models covered:Claude CodeOpenCode
Every shift starts with empty hands: it reads the logbook on the way in, and at a checked exit, a shift that changed things but wrote nothing is sent back to write.

What you will be able to do

  • Tell compaction loss from between-session loss and pick the fix for each.
  • Write a lookup rule and a store rule into the instruction file the agent loads.
  • Add a Stop hook that blocks the end of a session that edited files and stored nothing.
  • Test the hook with a synthetic transcript before trusting it.
  • Share one memory store across Claude Code, OpenCode and Kimi Code.

Key takeaways

  • Claude Code forgets context between sessions because every session starts with an empty context window; cross-session memory makes the agent look up past decisions before work and save new ones after.
  • Forgetting inside a session (compaction) and forgetting between sessions are different problems, and only the second is fixed by a memory routine.
  • Built-in memory files are context, not enforcement: we run a Stop hook that sends the agent back when it edited files and stored nothing.
  • The hook counts calls by name, so it proves a store happened, never that it was a good one.
  • OpenCode and Kimi Code read the same rules and reach the same store; we run no memory hook in either.

Claude Code forgets context between sessions because every session starts with an empty context window. Inside one session, a full window is compacted: older turns become a summary. Between sessions, nothing carries over except what was written down.

Cross-session memory is a routine that makes an AI coding agent look up past decisions before work and save new ones after, so a fresh context window starts informed. We run it as a loop in Claude Code, enforced by a Stop hook (a script Claude Code runs whenever it finishes responding). OpenCode and Kimi Code share the same rules and the same store.

Inside a session, older turns are compacted; between sessions, the new window starts empty and only stored notes cross over.

Why Claude Code Forgets, and What Our Memory Loop Does

Claude Code does not forget by accident. Every session starts empty by design, so the fix is a routine, not a bigger file.

Anthropic's docs(opens in new tab), 2026: "Each Claude Code session begins with a fresh context window." Two built-in features give Claude Code memory across sessions:

Built-inWhat it carriesSource (2026)
CLAUDE.mdInstructions you write, loaded at the start of every conversation. The project-root file is re-read from disk after /compactAnthropic, Memory(opens in new tab)
Auto memoryNotes Claude writes itself, kept per repository. The first 200 lines or 25 KB of MEMORY.md load at session startAnthropic, Memory(opens in new tab)
Both"Claude treats them as context, not enforced configuration."Anthropic, Memory(opens in new tab)

Context is something the agent weighs, not a step it has to take. So we added a loop on top, with a shared store the agent reaches over MCP:

PartWhat happens
TriggerA session starts work on something it does not already know
Recall, work, storeLook up memories and routes, do the task, save what was learned
CheckA Stop hook, at nearly every stop, sends the agent back if it edited files and stored nothing
OutputsMemories, routes and task notes the next session finds

The store is our company brain; how notes are sorted is Layer 1 of our self-improving system.

The copyable version (hook, settings entry and test) is in Build your own.

Why We Built It: A Written Rule Gets Skipped

We wrote the rule down and the agent skipped it. A rule that depends on the agent remembering to follow it is a suggestion.

The rule sat in the instruction file: look up before you explore, store after. Sessions skipped the lookup and re-read code an earlier session had already mapped. Sessions edited files and ended with nothing written down, so the next one rediscovered the same answer.

The memory existed and the tools worked. What was missing was a moment where skipping had a cost. A person approves the work that matters (our governance model), but nobody watches every session end.

See how ConvOps gives every agent one memory it checks before work(opens in new tab), and keeps the notes every session leaves behind.

How the Memory Loop Runs

Recall costs one call. Storing costs one call. The only expensive part is making both happen every time, so that is the job we gave the hook.

At each checked stop, edits with nothing stored send the agent back to store; otherwise the session ends.

How a Stop hook decides. It reads the transcript (the file where Claude Code logs every message and tool call) and ends with an exit code, a number telling Claude Code what to do next. Exit 0 lets the session end. Exit 2 "prevents Claude from stopping, continues the conversation" (Anthropic, Hooks(opens in new tab), 2026), and the agent reads the hook's message. Our delete guard uses the same exit code on tool calls.

StepWho runs itGate
RecallAgent, context_query, before exploring anything it does not already know. One call returns memories (what we know) and routes (where things live)Known answers skip it
WorkAgentNone
StoreAgent, memory_store, after every action. To replace a note: store the new one, then forget the old one. On a task action, a context= note stores 2 to 3 lines in the same callSkipping needs a reason
RouteAgent, after exploring filesNone
CheckThe Stop hookExit 2 sends the agent back

What the Memory Stop Hook Checks

Every threshold here is the one our hook runs.

RuleExact valueWhy it exists
When it runsEvery Stop event, 15 second timeout (default 600, Anthropic, Hooks(opens in new tab), 2026)The last moment a session can be held back
What it readsThe name of every tool call in the transcriptWhat the agent did, not what it said
Loop guardstop_hook_active is true: exit 0A hook that blocks its own retry never lets a session end
Small sessions passFewer than 3 tool calls: exit 0A two-call answer has nothing worth storing
IntervalAt least 2 assistant transcript entries since the last check. Claude Code writes one entry per content block, so the check runs at nearly every stopEach check reviews only what is new
Edited, stored nothingAn Edit, Write, MultiEdit or NotebookEdit call and zero memory_store calls, without waiting for the read-only rule belowA file change deserves a written why
No lookup at the first checkNo context_query or route_query callRecall first is the rule; later checks do not repeat it
Recall orderZero recalls and zero stores, stores but no recall, or a store since the last check that came before the first recallWithout a lookup first, the agent cannot see the note to replace, so it writes a duplicate
Read-only ruleRecalled, stored nothing, no edits, at least 4 transcript entriesExploring files is work worth storing
What countsRecall: names containing memory_recall or context_query. Store: names containing memory_storeMatching by name keeps the hook independent of the store

Each broken rule adds a message, the block ends with "This is mandatory.", and the hook exits 2.

What the Hook Cannot See

We trust the hook more because we know exactly where it is blind.

  • It counts tool calls by name. It never reads what a recall returned or whether a store was any good.
  • Only memory_store counts as a store. A session that writes only context= notes and edits files is still sent back.
  • Shell edits are not seen. A sed or a heredoc is not an edit to the hook; only the four native edit tools are.
  • The transcript may lag. Anthropic's hooks docs(opens in new tab) say it is written asynchronously, so a last-moment store is counted at the next check.
  • It runs in Claude Code only; see the OpenCode and Kimi Code sections.

Hooks hold the rules that must never depend on memory (the hooks layer). This one forces the calls, not the memory.

How Claude Code Remembers Between Sessions

The useful moment is not the store. It is the next morning, when a session that never saw Monday's work opens with Monday's lesson.

Copperfern is a fictional online homeware shop; its names, files and notes are invented, and the thresholds and messages are real.

Monday. Maya, who runs operations, files a task: "Spanish orders are one cent off at checkout." The coding agent runs context_query("spanish vat checkout rounding") and finds nothing about rounding. It reads shop/checkout/tax.py, sees VAT rounded once per order total, edits the file and its test to round per line, and tries to stop.

The hook runs: 5 tool calls, enough new entries since the last check. A lookup happened, so only the edit check fires:

KNOWLEDGE CHECK: Modified 2 file(s) but stored nothing. memory_store() the change/decision (supersede if updating). If you explored files, create_route()/update_route() in ConvOps. This is mandatory.

Exit 2. The agent stores a gotcha, importance 7, tags shop, checkout, vat. The note reads: "Spanish VAT rounds per line, not per order total; rounding the total is off by a cent on multi-item orders." It records a route: "VAT rounding lives in shop/checkout/tax.py". It stops again. stop_hook_active is true, and the session ends.

Monday's session fixes the rounding, is sent back once, and stores why before it ends.

Tuesday. A new task: "Add VAT to the gift-wrap line." New session, empty window: nothing from Monday is in it. The agent runs context_query("vat gift wrap checkout"), and Monday's gotcha and the route come back. It opens shop/checkout/tax.py directly and rounds the gift-wrap VAT on its own line.

Shipping turns out to be a separate line too. The agent stores the refined gotcha, "Spanish VAT rounds per line; gift wrap and shipping each count as their own line.", then forgets Monday's record with memory_forget, a reversible soft delete. At the stop, the lookup came before the store and the edit has a store: exit 0. Only the refined note answers the next lookup.

Tuesday's session never saw Monday, yet its first lookup returns Monday's lesson; it stores the refined note and forgets the old one.

What the Loop Leaves Behind

A memory is worth exactly what the next session can use without asking: one fact, its reason, a type and an honest importance.

A memory record, as the agent stores it:

content:    Spanish VAT rounds per line, not per order total; rounding the
            total is off by a cent on multi-item orders.
category:   gotchas
importance: 7
tags:       shop, checkout, vat
task_id:    cf-131

The content holds the fact and its reason. Importance is how much the next session loses if it misses the note: most sit at 4 to 6, and this is a 7 because a wrong total reaches customers. Two to five tags make it findable.

A route says where something lives:

title:       VAT rounding
description: VAT rounding lives in shop/checkout/tax.py
paths:       shop/checkout/tax.py, tests/test_tax.py

A context= note, written in the same call as a task update, stored as one memory linked to the task (importance 4 by default; under 40 characters it is not stored):

Rounded VAT per line, not per order; Spanish totals were a cent off on
multi-item orders.

No field replaces a record. The agent stores the new note and forgets the old one. A person can also replace a note on the brain page, which links the new record to the old and drops the old one from recall.

What Goes Wrong With a Memory Hook

Our enforcement failed twice, in opposite directions: once it let edits through silently, once it blocked forever. Check your own hook for both.

It let edits through. The only "stored nothing" check waited for a session to pass a minimum length. A short Copperfern session edits the shipping-label template and ends cleanly with nothing written; the next session redoes the same investigation. The rule it produced: edits with no store are flagged at the next check, without that wait. The 3-call floor and the interval still apply.

It blocked forever. The store-before-recall check compared the first store with the first recall across the whole session. A session stores a note before its first lookup, then looks up correctly, and is still sent back at every stop until it is abandoned. The rule it produced: each check looks only at what happened since the last check, so an early slip is reported at the check that covers it, not at every later stop.

Our code comment says why: "a flag no later behaviour can satisfy is a nag, not a guard." That rule now has a test that drives the real hook with a synthetic transcript, a fake session log.

Before the fix, one early store blocked every later stop; now each check looks only at what happened since the last one, so the slip is reported in its own window.

We narrowed the lookup rule. It used to apply to every message. Now it applies before exploring or working on something not already known; storing after every action stays mandatory. A session asked "what time is it in Madrid" runs no lookup; a session about to read the checkout code always does.

Build Your Own Memory Loop in Claude Code

Start with one rule the hook can count: if a session changed files, it writes down what and why before it ends.

First, connect the store. We use ConvOps; one command adds it to Claude Code:

claude mcp add --transport http convops https://mcp.convops.app/

Then add these two lines to your CLAUDE.md. Keep that file short (what to cut from a long CLAUDE.md): the hook carries the enforcement, the file carries the habit.

Before exploring or working on anything you don't already know, query the memory store first. Known answers and chat skip the lookup.
After every action, store what you learned and why, with a type, an honest importance and two to five tags. To replace a note, store the new one, then forget the old one.

This reduction of our hook (Claude Code 2.1.301) keeps the loop guard, the 3-call floor, the interval, the edit check and the first-check lookup. Save it as ~/.claude/hooks/memory-gate.py:

"""Stop hook: send the agent back when it edited files and stored nothing."""
import json, os, sys, tempfile

MIN_TOOL_CALLS = 3
INTERVAL = 2
EDIT_TOOLS = ("Edit", "Write", "MultiEdit", "NotebookEdit")

hook = json.load(sys.stdin)
if hook.get("stop_hook_active"):
    sys.exit(0)  # the retry after a block is not checked
names, entries = [], 0
with open(hook["transcript_path"]) as f:
    for line in f:
        entry = json.loads(line)
        if entry.get("type") != "assistant":
            continue
        entries += 1
        for block in entry.get("message", {}).get("content", []):
            if isinstance(block, dict) and block.get("type") == "tool_use":
                names.append(block["name"])

if len(names) < MIN_TOOL_CALLS:
    sys.exit(0)
counter = os.path.join(tempfile.gettempdir(), f"memory_gate_{hook['session_id']}")
last = int(open(counter).read()) if os.path.exists(counter) else 0
if entries - last < INTERVAL:
    sys.exit(0)
open(counter, "w").write(str(entries))
edits = sum(n in EDIT_TOOLS for n in names)
stores = sum("memory_store" in n for n in names)
lookups = sum("context_query" in n or "route_query" in n for n in names)
problems = []
if edits and not stores:
    problems.append(f"Modified {edits} file(s) but stored nothing. Store the change and why.")
if last == 0 and not lookups:
    problems.append("No lookup at the first check. Query the memory store first.")
if problems:
    print("KNOWLEDGE CHECK: " + " | ".join(problems) + " This is mandatory.", file=sys.stderr)
    sys.exit(2)

Register it under Stop in .claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "python3 ~/.claude/hooks/memory-gate.py", "timeout": 15 }
        ]
      }
    ]
  }
}

Test it with a synthetic transcript before you trust it. This one has a lookup and two edits, so it expects exit 2:

printf '%s\n' \
  '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"context_query"}]}}' \
  '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Edit"}]}}' \
  '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Edit"}]}}' \
  > copperfern-test.jsonl
echo '{"session_id":"test-001","transcript_path":"copperfern-test.jsonl"}' \
  | python3 ~/.claude/hooks/memory-gate.py; echo "exit $?"

Use a new session_id per run, because the counter file remembers the last check. Then add a memory_store line and expect exit 0.

OpenCode Memory Between Sessions

OpenCode 1.18.31 sessions also start empty. We give them the same text and the same store.

Our AGENTS.md files are symlinks to CLAUDE.md, so OpenCode reads the same recall and store rules (one file for both). Our opencode.json points its mcp block at the same ConvOps store, so a note stored from Claude Code comes back on an OpenCode lookup. We run no memory hook in OpenCode. It has a plugin surface for one (plugins(opens in new tab)); our delete guard shows the shape.

Kimi Code Memory Between Sessions

Kimi Code 0.31.1 gets the same text and the same store. It reads the symlinked AGENTS.md, and our .kimi-code/mcp.json lists the same ConvOps server, so its lookups return what Claude Code and OpenCode stored. We run no memory hook in Kimi Code. It takes [[hooks]] entries in its user config (Kimi Code hooks(opens in new tab)), and the delete guard guide shows one.

Create a free ConvOps account and connect Claude Code, OpenCode or Kimi Code to one memory(opens in new tab), then add the lookup rule, the store rule and the Stop hook.

Steps

  1. Connect a memory store over MCP

    Pick a store your agent reaches as an MCP server, so every session and every tool reads and writes the same notes. We use ConvOps: create a free account at my.convops.app/register and add it to Claude Code as an MCP server.

  2. Write the lookup rule

    Add one line to CLAUDE.md: before exploring or working on anything not already known, query the memory store first. Known answers and chat skip the lookup.

  3. Write the store rule

    Add one line to CLAUDE.md: after every action, store what was learned and why, with a type, an honest importance and two to five tags. To replace a note, store the new one, then forget the old one.

  4. Add the Stop hook

    Save the hook script as ~/.claude/hooks/memory-gate.py. It skips the retry after a block and sessions under 3 tool calls, checks at most once per 2 assistant transcript entries, and exits 2 when the session edited files and stored nothing.

  5. Register it under Stop

    Add the hook to the Stop list in .claude/settings.json with a 15 second timeout, so it runs every time Claude Code finishes responding.

  6. Test it with a synthetic transcript

    Write a short JSONL transcript with a lookup and two edits, pipe a payload into the script and expect exit 2. Add a memory_store line, use a new session_id, and expect exit 0.

Frequently asked questions

What is cross-session memory for an AI coding agent?

Cross-session memory is a routine that makes an AI coding agent look up past decisions before work and save new ones after, so a fresh context window starts informed. ConvOps holds the shared store: the agent queries it over MCP before exploring, and writes memories, routes and task notes to it after each action. At Neomanex, a Stop hook in Claude Code sends the agent back when it edited files and stored nothing.

Who needs cross-session memory?

Developers and dev leads need it when their Claude Code, OpenCode or Kimi Code sessions repeat work or reopen decisions an earlier session already settled. ConvOps keeps those decisions in one store that every session and every tool can query. The result is that the next session starts with what the last one learned, instead of re-reading the same code to rediscover it.

Does Claude Code save context between sessions?

Claude Code keeps only what is written down, so Neomanex stores each session's lessons in ConvOps. Every Claude Code session starts with an empty context window. CLAUDE.md and auto memory load at the start, and Anthropic's docs say Claude treats them as context, not enforced configuration. Anything else the agent learns is lost unless it is stored where the next session looks it up.

How do I make Claude Code remember between sessions?

Create a free ConvOps account at my.convops.app/register, then connect it to Claude Code as an MCP server. Add two lines to CLAUDE.md: look up the store before exploring anything not already known, and store what was learned after every action. Then add a Stop hook that blocks the end of a session that edited files and stored nothing, and test it with a synthetic transcript.

Does OpenCode remember between sessions?

OpenCode sessions start stateless, so Neomanex connects OpenCode to ConvOps over MCP, the same store Claude Code uses. Our AGENTS.md files are symlinks to CLAUDE.md, so OpenCode reads the same lookup and store rules. The constraint: we run no memory hook in OpenCode, so storing depends on the agent following the text. OpenCode has a plugin surface for such a hook; we do not run one there.

Can a hook force Claude Code to save what it learned?

A hook forces the call, not the quality. In our setup, a Claude Code Stop hook counts memory_store calls by name and blocks the stop with exit 2 when a session edited files and stored nothing, and ConvOps receives the store. The hook never reads what was stored, so a weak note passes. Only memory_store counts, and edits made through the shell are not seen.

Does Claude Code share context between chats?

Separate Claude Code sessions do not share a context window, but they can share a store, and ours is ConvOps. What one session stores in ConvOps, another session finds on its first lookup, even in OpenCode or Kimi Code. CLAUDE.md and auto memory also load in every session of the same repository, as context the agent weighs rather than rules it must follow.