Your Coding Agent Doesn't Have Memory. It Has a Really Good Notebook.

Everyone assumes coding agents remember your project with embeddings. They don't. Here's the actual mechanism: static files, capped indexes, and silent context rewrites.

Your Coding Agent Doesn't Have Memory. It Has a Really Good Notebook.

Ask ten developers how Claude Code or Cursor "remembers" their project, and eight will say some version of "embeddings" or "a vector database somewhere." They're wrong. I went and read the actual source and docs to check.

None of the serious coding agents use vector search as a core memory primitive. Not Claude Code, not Cursor, not Aider, Goose, OpenHands, Gemini CLI, or Codex CLI. What they actually run is three plain mechanisms stacked on top of each other: a static file that's always in the prompt, an index-plus-detail-file split for anything learned mid-session, and a summarization pass that rewrites the conversation before it overflows the context window. No embedding model, no similarity search, just files and a re-read.

Mechanism 1: the static file that's always in the prompt

Every agent starts with a specially named markdown file, discovered by walking up from your working directory to a project root (plus a global user-level copy), concatenated verbatim into the system prompt. Claude Code calls it CLAUDE.md. Codex CLI and OpenCode call it AGENTS.md. Gemini CLI calls it GEMINI.md. Goose calls it .goosehints. Pi loads either AGENTS.md or CLAUDE.md, with a project-local AGENTS.override.md taking precedence over both.

This file is never rewritten by the model. It's pure config, and that's the point: because it lives in the static portion of the prompt, re-read from disk on every request instead of being carried as conversation history, it survives compaction automatically. Claude Code's own docs state this outright: "Project-root CLAUDE.md survives compaction: after /compact, Claude re-reads it from disk and re-injects it into the session." Nothing special has to happen for that to work. It's a side effect of where the bytes come from.

Some implementations get path-scoped about it. Claude Code and Pi both support nested CLAUDE.md/AGENTS.md files in subdirectories, which load lazily as the model reads files in that subtree, and reload after compaction "as Claude reads files they apply to." Cursor's .mdc rule files take this further with frontmatter fields (alwaysApply, globs) that make even the static layer conditionally scoped. OpenHands' repo.md is the always-on baseline. Its .openhands/microagents/knowledge/*.md files are the same idea gated behind a triggers: [...] list in the YAML frontmatter, only entering context when the trigger word shows up in the conversation.

Mechanism 2: index file plus detail files, not one giant blob

Static instructions are fine for rules. They're the wrong shape for facts learned mid-session: "this endpoint returns cents, not dollars," "staging deploys need --force-migrate," "the user corrected me twice about tabs vs spaces." Stuff that into the always-loaded file and you're burning context on it every single turn whether it's relevant or not.

Claude Code's Auto Memory system, shipped since v2.1.59, solves this with a two-tier split that's worth copying directly. The layout, straight from the docs:

~/.claude/projects/<project>/memory/
├── MEMORY.md           # index, one line per memory, loaded into every session
├── user_role.md        # one memory
├── feedback_testing.md # one memory
└── ...                 # any other topic files
MEMORY.md is hard-capped: the first 200 lines or 25KB, whichever comes first, load at session start. Everything past that threshold doesn't load automatically. Topic files have no cap and load only when the model decides one is relevant and calls its ordinary Read tool on it, exactly the same tool it uses to read your source files. No bespoke recall_memory API. The retrieval mechanism is: the model already has a file-reading tool, so give it a filename.

Memories are typed on write, via a type field in frontmatter, into a fixed four-way taxonomy: user (role, preferences), feedback (corrections, confirmed approaches), project (context not derivable from code or git, like deadlines and incident history), reference (pointers to external systems like a ticket tracker). This taxonomy exists to give the model an unambiguous rule for which file a new fact belongs in, and there's an explicit save filter attached to it. Claude "skips anything it can derive from the codebase, such as architecture, file paths, or debugging fixes," and skips anything the project's CLAUDE.md already states.

The community has already ported this exact design onto other harnesses. pi-memory-cc, an extension for the Pi coding agent, replicates it almost line for line: same four-type taxonomy, same 200-line/25KB dual cap on the index, same two-step write protocol (topic file first, then the index line), and the same git-root resolution so worktrees of one repo share a memory directory instead of forking it per checkout. It hooks Pi's before_agent_start event, rebuilds the injected system-prompt block only when MEMORY.md's mtime has changed, and lets the model write memory files directly with the Write tool. No extra LLM call, no post-session extraction pass.

Cline and Roo Code take a cruder version of the same idea and call it Memory Bank: six fixed files (projectbrief.md, productContext.md, activeContext.md, systemPatterns.md, techContext.md, progress.md) that the model is instructed, via a system prompt with lines like "I MUST read ALL memory bank files at the start of EVERY task, this is not optional," to fully reread on every task. No index, no on-demand loading, no size cap beyond "keep it to a page." It works because the instruction is aggressive and the file count is small. It stops working the moment either of those stops being true.

Goose bolts a real query layer onto the same problem instead of an index. .goosehints stays static and always-loaded, same as everyone else's config file. But its Memory Extension is an MCP server: the model detects tags or keywords in the user's request, then issues an MCP call that fetches matching entries from ~/.goose/memory on demand. It's the closest thing to actual retrieval in this entire category, and it's also the only implementation here that needed a dedicated server process to pull it off, instead of reusing a file-read tool that was already sitting there.

Mechanism 3: the conversation gets silently rewritten before it overflows

None of these agents hold your six-hour session in the model's head verbatim. Every one with a context limit, which is all of them, runs some version of the same cut, summarize, splice loop once the transcript gets too big.

Pi's implementation is the most fully documented one I found, and it's worth walking through because the edge cases it handles are exactly the ones that break naive summarizers. Auto-compaction triggers when contextTokens > contextWindow - reserveTokens, with reserveTokens defaulting to 16384 and a keepRecentTokens budget of 20000 (both configurable in .pi/settings.json). The algorithm:

  1. Walk backward from the newest message, accumulating token estimates until keepRecentTokens is reached. That's the cut point.
  2. Collect everything from the previous compaction's kept boundary up to that cut point.
  3. Call an LLM to summarize it, feeding the previous compaction's summary back in as context so information doesn't decay across repeated rounds.
  4. Append a CompactionEntry to the session (never insert mid-file) containing the summary text and a firstKeptEntryId marking where verbatim messages resume.
  5. Rebuild the context sent to the model as: system prompt, then the summary, then messages from firstKeptEntryId onward.
Cut points are constrained: only user messages, assistant messages, bash-execution messages, or custom messages qualify, and a tool call is never separated from its tool result. When a single turn is bigger than the entire keepRecentTokens budget (a "split turn"), Pi cuts mid-turn at an assistant-message boundary instead and generates two summaries that get merged: one for prior history, one for the truncated turn prefix. The summary format is a fixed template (Goal, Constraints & Preferences, Progress with Done/In Progress/Blocked, Key Decisions, Next Steps, Critical Context, plus <read-files>/<modified-files> blocks), and file operations tracked in those blocks accumulate cumulatively across every compaction in the session, so "what has this session touched" survives no matter how many times the raw transcript underneath it gets discarded.

Other agents converge on nearly the same shape with different knobs. Gemini CLI's /compress fires automatically at 70 to 80 percent of the context window and produces a structured summary preserving goal, key knowledge, file state, and current plan. Codex CLI runs two separate paths: a local one gated by model_auto_compact_token_limit in ~/.codex/config.toml (roughly 90 percent of the model's context window, with a configurable compact_prompt override), and, for OpenAI-hosted models, a server-side POST /v1/responses/compact call that returns an opaque AES-encrypted blob the client never inspects and simply replays on the next request. OpenHands frames the whole thing as a pluggable Condenser abstraction: LLMSummarizingCondenser triggers past a configurable event count, always keeps the first keep_first events (system prompt, initial task), and summarizes the rest.

The extensibility point worth noting for anyone building on top of one of these harnesses: Pi exposes session_before_compact as a hook that receives the exact messages about to be summarized, the previous summary, the token count, and a cancel-or-override return value. That lets an extension swap in a different model or a completely different summarization strategy without touching the core compaction logic. It's the same shape as Codex's compact_prompt override, just exposed as code instead of a config string.

Where the vector databases actually are

I went looking for the sophisticated stuff assuming it was hiding somewhere in Claude Code or Cursor's internals: semantic search over commit history, a knowledge graph of the codebase, an embedding index refreshed on every save. It isn't there, not as a core primitive, in any of the eight harnesses I checked.

The closest thing to genuine retrieval built into any of them is OpenHands' keyword-triggered microagent frontmatter, and that's deterministic string matching against a triggers: list, not a nearest-neighbor search over embeddings. Aider's repo map, which people sometimes mistake for a RAG system because it "understands the codebase," is tree-sitter symbol extraction plus a PageRank-style graph over call signatures. Deterministic, no embedding index to maintain, explicitly chosen over vector search by Aider's own maintainers for that reason.

Every place real vector search shows up in this space is a third-party plugin layered on afterward through MCP: Supermemory and Hindsight for Gemini CLI and OpenCode, MemPalace for OpenCode. They exist because someone wanted cross-project or cross-user search at team scale, which is a different problem from "remember what this one repo's session learned." None of them are load-bearing for the base product.

The rule doing more work than any of the tooling

Every well-specified implementation of this pattern enforces the same filter at write time, and it matters more than the storage format around it. Claude Code states it directly: skip anything derivable from the codebase or git history, skip anything the project's instructions file already covers, and only persist something if it would help a future session that's starting from zero context. pi-memory-cc copies the same rule almost word for word, calling it "anti-drift guidance" in its README.

Remove that filter and an index-plus-topic-file system degrades into the same failure mode as an unbounded chat log: it fills up with restated facts nobody needed written down, and the always-loaded index stops being cheap to read. Keep it, and a hard-capped markdown index, a folder of on-demand text files, and a summarization pass that respects tool-call boundaries turn out to be the entire memory stack behind every coding agent currently shipping.