# 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.

By David Marsa, Founder & CEO. Updated 2026-10-26.

## 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](/directory/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.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/claude-code-forgets-context-between-sessions-two-ways.png)

## 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](https://code.claude.com/docs/en/memory), 2026: "Each Claude Code session begins with a fresh context window." Two built-in features give Claude Code memory across sessions:

| Built-in | What it carries | Source (2026) |
|---|---|---|
| CLAUDE.md | Instructions you write, loaded at the start of every conversation. The project-root file is re-read from disk after `/compact` | [Anthropic, Memory](https://code.claude.com/docs/en/memory) |
| Auto memory | Notes Claude writes itself, kept per repository. The first 200 lines or 25 KB of `MEMORY.md` load at session start | [Anthropic, Memory](https://code.claude.com/docs/en/memory) |
| Both | "Claude treats them as context, not enforced configuration." | [Anthropic, Memory](https://code.claude.com/docs/en/memory) |

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:

| Part | What happens |
|---|---|
| Trigger | A session starts work on something it does not already know |
| Recall, work, store | Look up memories and routes, do the task, save what was learned |
| Check | A Stop hook, at nearly every stop, sends the agent back if it edited files and stored nothing |
| Outputs | Memories, routes and task notes the next session finds |

The store is our [company brain](/learn/guides/run-a-company-with-ai-agents); how notes are sorted is [Layer 1 of our self-improving system](/learn/guides/build-self-improving-ai-system).

The copyable version (hook, settings entry and test) is in [Build your own](#build-your-own-memory-loop-in-claude-code).

## 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](/learn/guides/ai-governance-operating-model)), but nobody watches every session end.

[See how ConvOps gives every agent one memory it checks before work](https://convops.app), 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.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/claude-code-forgets-context-between-sessions-loop.png)

**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](https://code.claude.com/docs/en/hooks), 2026), and the agent reads the hook's message. Our [delete guard](/learn/guides/stop-claude-code-deleting-files) uses the same exit code on tool calls.

| Step | Who runs it | Gate |
|---|---|---|
| Recall | Agent, `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 |
| Work | Agent | None |
| Store | Agent, `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 call | Skipping needs a reason |
| Route | Agent, after exploring files | None |
| Check | The Stop hook | Exit 2 sends the agent back |

### What the Memory Stop Hook Checks

Every threshold here is the one our hook runs.

| Rule | Exact value | Why it exists |
|---|---|---|
| When it runs | Every `Stop` event, 15 second timeout (default 600, [Anthropic, Hooks](https://code.claude.com/docs/en/hooks), 2026) | The last moment a session can be held back |
| What it reads | The `name` of every tool call in the transcript | What the agent did, not what it said |
| Loop guard | `stop_hook_active` is true: exit 0 | A hook that blocks its own retry never lets a session end |
| Small sessions pass | Fewer than 3 tool calls: exit 0 | A two-call answer has nothing worth storing |
| Interval | At least 2 assistant transcript entries since the last check. Claude Code writes one entry per content block, so the check runs at nearly every stop | Each check reviews only what is new |
| Edited, stored nothing | An `Edit`, `Write`, `MultiEdit` or `NotebookEdit` call and zero `memory_store` calls, without waiting for the read-only rule below | A file change deserves a written why |
| No lookup at the first check | No `context_query` or `route_query` call | Recall first is the rule; later checks do not repeat it |
| Recall order | Zero recalls and zero stores, stores but no recall, or a store since the last check that came before the first recall | Without a lookup first, the agent cannot see the note to replace, so it writes a duplicate |
| Read-only rule | Recalled, stored nothing, no edits, at least 4 transcript entries | Exploring files is work worth storing |
| What counts | Recall: names containing `memory_recall` or `context_query`. Store: names containing `memory_store` | Matching 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](https://code.claude.com/docs/en/hooks) 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](/learn/guides/build-self-improving-ai-system)). 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.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/claude-code-forgets-context-between-sessions-monday.png)

**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.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/claude-code-forgets-context-between-sessions-tuesday.png)

## 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:

```text
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:

```text
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):

```text
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.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/claude-code-forgets-context-between-sessions-nag.png)

**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:

```bash
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](/learn/guides/claude-md-too-long)): the hook carries the enforcement, the file carries the habit.

```text
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`:

```python
"""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`:

```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:

```bash
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](/directory/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](/learn/guides/claude-md-too-long)). 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](https://opencode.ai/docs/plugins)); our [delete guard](/learn/guides/stop-claude-code-deleting-files) 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](https://www.kimi.com/code/docs/en/kimi-code-cli/customization/hooks.html)), and the delete guard guide shows one.

[Create a free ConvOps account and connect Claude Code, OpenCode or Kimi Code to one memory](https://my.convops.app/register), 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.

## FAQ

### 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.

Canonical: https://neomanex.com/learn/guides/claude-code-forgets-context-between-sessions
