Skip to content

Memory

Memory records what you worked on so a later session can recall decisions, fixes and unfinished work. It is on by default and independent of quizzes: quiz.enabled: false stops questions while memory continues.

Start by asking /eklavya:memory about a past change. To browse, use the Memory dashboard. To check capture or a stalled queue, run eklavya memory status.

What gets captured

Hooks record prompts, Claude’s final message for each turn, session events and tool activity. A tool event includes its name, arguments, file paths and what came back: a failed call keeps its error (up to 800 characters), and a successful call keeps an excerpt of its result, the first 1,600 and last 600 characters. Reads and edits keep no result, so a file’s contents are never stored as a second copy of your repository.

The result is what lets an observation say what was found, not only what ran: kubectl get configmap is remembered with the value it printed. It also means command and query output is stored locally after redaction. To keep a tool’s output out of memory entirely, for example a database query tool that returns sensitive rows, add it to privacy.exclude_tools.

Event kindSource
promptA message you send.
tool_use, tool_errorSuccessful tool calls with a result excerpt, or failed calls with their error. A failure is recorded only when the PostToolUse hook reports it: Eklavya does not register PostToolUseFailure, so a failure a host sends only there is not captured, and transcript replay records tool calls without outcomes.
file_editEdit, Write, NotebookEdit and MultiEdit.
file_readRead, NotebookRead, Glob and Grep.
assistant, lifecycleClaude’s final message at the end of each turn (from Claude Code’s Stop hook), and session start/stop events.
noteA note saved explicitly.

Bodies are limited to 4,000 characters after redaction; edits retain up to 600 characters per side. memory.capture: minimal keeps prompts, assistant messages, session events and notes. off stops capture but keeps existing history searchable.

What is never captured

Privacy checks run before evidence is stored. Built-in path exclusions are substring matches:

.env .env. /.ssh/ id_rsa id_ed25519 .pem
.p12 .keystore credentials.json .netrc .npmrc
secrets. .eklavya/knowledge.db

Nothing is captured in a folder outside a git checkout unless retrieval.cross_project is on: only a cross-project search serves that folder, so otherwise its evidence would only take up space. An excluded read or edit is dropped when it has no remaining allowed file. Any other event that names an excluded file, including a failed Read, Edit or Write, is dropped whole even when it also names an allowed file, because its error or result can quote the excluded file and cannot be split by file. A failure on an allowed file is still recorded. Earlier releases kept the error text of a failed Read, Edit or Write of an excluded file. The first time the database opens after an upgrade, a migration deletes that evidence. It does not change summaries already written from it; delete any affected entry with memory_delete. Eklavya’s own tool calls, CLI traffic and checkpoint messages are excluded to prevent its output feeding back into memory.

Secret detection redacts private keys, URL passwords, common provider tokens, JWTs, bearer tokens and secret assignments such as DB_PASSWORD=…. It preserves markers such as [redacted:github-token] so the surrounding event remains understandable. The built-in rules also apply to saved notes and imported entries. Redaction, including your privacy.redact_patterns, runs before any excerpt or length limit cuts the text: a quoted secret whose closing quote was cut off is still redacted, and the partial word at a cut is dropped so a shortened token cannot slip past its pattern.

Built-in secret shapes
ShapeExamples
Private keysPEM private-key blocks, including unfinished blocks.
URL credentialsPasswords in scheme://user:password@host.
Anthropic, OpenAI and Stripesk-ant-, sk-, sk-proj-, sk-svcacct-, sk-admin-, Stripe sk_ and rk_ forms.
GitHub, GitLab and npmghp_, gho_, ghu_, ghs_, ghr_, github_pat_, glpat-, npm_.
AWS, Slack and GoogleAKIA, ASIA, xoxb-, xoxa-, xoxp-, xoxr-, xoxs-, AIza.
Other credentialsThree-part JWTs, bearer tokens, and values assigned to names ending in password, passwd, passphrase, secret, credential, token, API/access/private key or authorization.

Add rules with privacy.exclude_paths, privacy.exclude_tools and privacy.redact_patterns. They extend the built-in rules; invalid regular expressions are skipped.

How observations are made

An observation summarises a batch of evidence into a title, narrative, facts, files and tags. Batches close:

  • At memory.batch_max_events events (40 by default).
  • At a turn end when they contain at least eight events or are 20 minutes old.
  • At the next session start, even if a smaller batch remains. Compaction is the same session, not a new start.

The default local-v1 summariser makes no network calls. It extracts and counts what evidence says rather than inferring why a change happened; its observations carry confidence 0.5. Types include bugfix, refactor, decision, feature, discovery, change and explicit note.

An optional providers.observer uses a Claude model through claude -p and your Claude Code login. This sends captured work to the model. Hooks queue the work and return without waiting.

The model is told to record what was learned, built, fixed, decided or configured rather than what the agent was doing, and to skip routine operations such as status checks with nothing to report, clean installs and searches that found nothing. It writes one observation per distinct finding, decision or change, up to six per batch, each with at most 12 facts. When it goes past those limits, the extra is trimmed and the rest is kept; only output with the wrong shape fails the batch.

Only one model worker runs at a time. It processes up to four batches and hands remaining ready jobs to a fresh worker. Calls have a 100-second limit; cancellation or timeout also stops child processes. Observer helper sessions are excluded from capture.

When the model is unavailable

If Claude Code is not logged in, a usage limit is reached or claude cannot be found, the queue pauses instead of retrying every job. While it is paused:

  • Each session start and turn end writes local-v1 observations for up to four waiting batches, so recall keeps showing current work. These are stand-ins: the jobs stay queued, and when the model later summarises a batch, its observations replace the local ones.
  • At most every 30 minutes, a turn end or session start checks whether the cause is fixed. A login is checked with claude auth status, which makes no model call. A usage limit is retried after an hour. Once the check passes, the queue resumes by itself.
  • The session banner says so, for example Memory paused · claude not logged in since 23 Sep · local summaries meanwhile · fix it, then: eklavya memory process.

Separately, if the oldest waiting job is more than six hours old, the banner shows Memory behind · 12 jobs waiting, oldest 9h · run: eklavya doctor.

NeedCommand or setting
See queue, worker and provider healtheklavya memory status
Resume now after fixing login, quota or a missing claude command, instead of waiting for the next checkeklavya memory process
Stop the current worker and return its unfinished job to the queueeklavya memory stop
Inspect, quarantine, restore or discard selected jobseklavya memory backlog
Return to local summariesSet providers.observer to null.

Clearing the observer or disabling memory cancels the active provider call without discarding queued jobs. See the CLI reference for selectors and options.

Session summaries and checkpoints

Each session that did durable work has one summary entry, rewritten as the session grows.

  • With a model observer, the summary is a checkpoint. When a batch ends a turn (it contains Claude’s final message), the same summarising call also writes where the session stands: Request, Investigated, Learned, Completed and Next steps. The model is shown the session’s previous checkpoint and the observations already recorded, so the checkpoint covers the whole session, not only its last few minutes. No extra model call is made.
  • Without one, the summary rolls up the session’s observations locally and is titled with the first thing you asked.

If checkpoints stop partway through a session (the observer is removed or paused, or its end-of-turn batches fail) while work is still recorded, the local roll-up replaces the older checkpoint so the summary never shows next steps the session has already moved past.

A session that recorded no observations, such as a quick question, gets no checkpoint, so it never replaces the next steps of the session before it. Summaries imported from Claude Mem use the same sections and are read the same way.

How recall works

At session start, Claude receives recent project history in an <eklavya-memory> block, in three parts that read top to bottom as the story of the last few days:

  1. A timeline. Up to 50 recent entries, oldest first and grouped by day, one line each: [#id] time type · title. A session line stands for a whole session: what was asked in it, or, when that is no longer on record, its first observation. This part has its own allowance of about 1,100 estimated tokens.
  2. The newest work in full, under Latest, in full:.
  3. Where the last session left off: the newest summary of an earlier session in this checkout, usually a checkpoint ending in its next steps. It is left out when a later session did newer work without a summary yet, and it never comes from another checkout, even with retrieval.cross_project on. A resumed or compacted session does not see its own summary here. Each checkpoint section is cut at 350 characters (a local roll-up at 1,750 in all); memory_get returns the full text.

Parts 2 and 3 share retrieval.max_tokens (1,200 estimated tokens by default) and retrieval.max_items (6 by default). The checkpoint is counted first. An entry too long for the remaining budget is skipped and shorter ones fill the space; nothing is sent both in full and as a timeline line. The block names its receipt in its header (receipt="123") and says when to look further: when the task needs an earlier decision, fix or unfinished work, read a matching entry with memory_get({ids: [<id>], receipt_id: 123}). The timeline adds that older work can be found with memory_search. It is a condition, not an instruction to call a tool every turn. The block is marked as evidence to verify, never instructions to obey. Entries a correction replaced are left out of recall; memory_timeline still lists them as the audit trail. Session-start recall is independent of questions: it is sent with questions off and in a session whose questions were silenced.

### 2026-09-25
[#11] 17:40 session · Add row-limit checkbox to CSV export
[#12] 17:52 bugfix · CSV export ignored the unchecked row limit
### 2026-09-26
[#14] 14:10 discovery · QA auth configmap has REFRESH_ENABLED=false
Latest, in full:
1. [#15] ACCESS_TTL of 600s causes logout when refresh is disabled — discovery, 2026-09-26
…
Where the last session left off ([#19], 2026-09-26 16:05):
Request: Find why QA users get logged out after 10 minutes
…
Next steps: set REFRESH_ENABLED=true in the QA configmap and verify with a 15-minute idle test

The session banner says what was delivered, for example Memory · 3 past entries + 45 titles recalled (~2.1k tokens), counting the full entries and the timeline lines. A project with only a few entries shows no titles, for example Memory · 3 past entries recalled (~1.1k tokens). It prints no memory line when nothing was recalled.

A resumed session gets back its whole transcript, so on resume the block leaves out every entry the session itself wrote, in the timeline and in full. After a compaction those entries can be the detail the summary dropped, so they are still recalled.

When you send a prompt

A prompt with at least 25 characters of your own words (pasted blocks and background-agent reports are ignored) is searched against the project’s memory. An entry is recalled only if it contains every word of the prompt or is close in meaning, within a third of retrieval.max_items and retrieval.max_tokens. Prompt recall never repeats an entry this session was already handed, and never recalls an entry this session wrote: that work is already in Claude’s context. When nothing qualifies, nothing is added.

When Claude opens a file

Before Claude reads a file this project has history about, Eklavya adds that file’s past work: up to 8 observations, one dated line each, with their IDs. Entries about that file specifically come before ones that only listed it among many. It happens once per file per session, only for files of at least 1,500 bytes, never for a subagent’s reads, and never for session summaries. The block names a receipt like other recall, so a memory_get it leads to is linked to it. It adds context only: whether the read is allowed is still up to Claude Code and your permission settings.

Recall needs a git checkout. In a folder outside git there is no project to recall from, so neither session-start nor prompt recall runs, and nothing is captured there unless retrieval.cross_project is on. Evidence that earlier versions captured outside git stays under No repository and is not summarised.

Relevant memory can also be supplied on a substantive prompt later in the session. It gets a third of the session-start budget (at most retrieval.max_items / 3 entries and retrieval.max_tokens / 3 estimated tokens, so two entries and 400 tokens by default), excludes entries already delivered and requires a relevance match. The cap is strict. When a matching entry is larger than the room left, it is sent as an excerpt instead of being skipped, provided it has a narrative and at least about 60 estimated tokens remain. The excerpt keeps the title and the start of the narrative, leaves out facts and files, and ends … [excerpt: memory_get #<id> for the rest]. Otherwise the entry is skipped. Unrelated prompts receive no memory block. Only what you typed is searched: pasted content, subagent hand-backs and task notifications are ignored, so a prompt that contains nothing else gets no memory block.

ModeBest useLimit
keywordExact names, identifiers and error strings.SQLite full-text search needs word boundaries.
semanticSimilar spellings, word forms and character sequences.Local vectors do not understand synonyms or conceptual meaning.
hybrid (default)Combine keyword and local-vector results.Its vector component has the same limits as semantic.

The local embedder, local-hash-v1, uses 256-dimensional vectors of tokens and character four-grams. It can relate “migration” to “migrations”, but cannot infer that “cookie” and “session token” refer to related ideas. The configured providers.embeddings field currently does not enable an external model: indexing and search remain local.

The vector search scans at most the newest 5,000 eligible entries; keyword search can reach older entries too. Hybrid keeps that keyword path. For Japanese, Chinese and other unspaced scripts, keep hybrid or use semantic, because character matching can find text the keyword tokenizer misses.

Search defaults to the current project. retrieval.cross_project widens search and session-start recall; an individual search can request all_projects.

Subagents

Recalled blocks reach the main conversation only. A subagent Claude delegates to starts without them, so when memory is on and the project has entries, it is told in one line to search memory with memory_search and read matches with memory_get if its task depends on earlier decisions, fixes or unfinished work. That line does not depend on whether questions are on or silenced for the session, and the tutor subagent receives it too.

What the savings percentage measures

Each recall records a receipt with two estimates, both using four characters per token (chars4-v1):

CountMeaning
Base (B)The raw evidence behind the recalled entries, deduplicated by event. This is an estimate of what reading that evidence would cost.
Delivered (D)The actual recalled block, its wrapper and any later detail fetch charged to it.

Estimated context reduction is (B − D) / B, shown by the dashboard and eklavya memory status. Receipts from the observer’s own claude -p helper sessions are excluded. A negative saving is reported as overhead; unavailable delivery is not counted as a saving.

A receipt records how far its context got:

DeliveryMeaning
preparedThe block was built but the hook has not written it, or never did. Never counted as a saving.
emittedThe hook wrote the block to Claude Code. Only emitted receipts contribute to the headline.
confirmedWritten by earlier releases for every recall when it was prepared. Counted like emitted.

Claude Code does not acknowledge hook context, so no receipt claims the model received or used it. A recall marks its entries as already handed to the session only once emitted, so a prepared block that was never written does not stop the next recall from offering them. Session-start, prompt and file recall each write a receipt; file recall shows titles only, so its receipt claims no saving of its own, but its size counts toward delivered (D) and lowers the headline percentage.

Each recall block names its receipt (receipt="123") and shows the call that uses it, for example memory_get({ids: [42], receipt_id: 123}). That links the detail fetch to the recall. Every memory_search, memory_get, memory_timeline and memory_file_history call is also logged locally, with or without a receipt: the tool, project, session, returned entry IDs, outcome (ok, empty or error), latency and estimated result size. The log never stores the query or the content returned. memory_status, eklavya memory status and the dashboard’s Reuse page report these reads, including how many are not linked to a receipt. Only memory_get can be linked, so every search, timeline and file history call counts as unlinked, as does a memory_get whose receipt no longer exists. A read shows that memory was fetched, not that Claude applied it correctly.

This estimates context volume compared with raw evidence. It is not API billing, money saved or prompt-cache usage, and does not claim Claude would otherwise have read every event. Base includes whole-file snapshots stored with edits, so it overstates what reading the history would really cost; treat the percentage as an upper bound.

The memory tools

Claude uses these tools on your behalf. Search returns compact IDs and titles; memory_get then reads selected entries. All accept optional cwd to identify the project.

ToolArguments and result
memory_searchquery; optional mode, type, tag, since, until, limit, all_projects. Returns IDs, titles, dates and files. Defaults to retrieval.max_items, maximum 50.
memory_getids (1–20), optional include_evidence, receipt_id, all_projects. Reads narratives, facts and evidence links. An ID that belongs to another project is listed under other_project instead of read, unless all_projects is true; each entry names its project. Raw evidence bodies are capped at 1,500 characters; the receipt charges this extra reading to the original recall. An unknown or expired receipt_id is ignored: the entries are still returned, with charged_to: null.
memory_timelinelimit, offset, type, since, until, session_id. Returns the newest entries, including superseded ones. Default 20, maximum 100.
memory_file_historyfile, optional limit. Matches path fragments; default 20, maximum 100.
memory_statusCapture, queue, provider and reuse health, plus explicit read counts (reads) and host_acknowledgement: "unavailable".
memory_writetitle, body, optional tags, files. Saves a redacted note.
memory_correctid and a corrected title, body, or both. Creates a replacement while preserving the original audit trail. The replacement and the supersession are one write: if either fails, the original stays the only live entry. A second correction of the same entry returns already_superseded.
memory_deleteid, optional hard. Soft deletion removes an entry from retrieval and from the keyword index, and repeating it keeps the first deletion time; hard: true erases it permanently, including an entry already soft-deleted.

memory_correct and memory_delete change only the current project’s entries. An ID from another project returns error: "other_project" with that project’s key and changes nothing; to change it, pass a cwd inside that project. A worktree counts as its main checkout’s project. | memory_collections | Saves and rebuilds named views; details below. | | code_outline | file, optional line. Lists declarations or expands around a line. | | code_find_symbol | name, optional limit (default 20, maximum 50). Finds declarations by case-insensitive partial name. |

Collections

memory_collections supports list, create, show, rebuild and delete. Every action except list requires name.

A collection saves a filter: optional description, query, type, tag, since and all_projects. show returns titles and IDs, with limit defaulting to 50 and capped at 200. Fetch the entries you need with memory_get.

Every filter field applies the same way with or without a query. A collection with only tag: "auth" holds entries tagged auth and nothing else, newest first. Deleted and corrected entries are never members.

Membership is fixed at the last rebuild, and show hides any member deleted or corrected since. A correction’s replacement does not join automatically, because it may no longer match the filter; it joins at the next rebuild if the filter admits it.

rebuild reruns the saved filter. If it would empty a previously populated collection, the previous membership is preserved and marked failed; use force only when the empty result is intended. Deleting a collection leaves its entries untouched.

Code exploration

The code tools help Claude choose which source to read. They scan declarations line by line rather than parsing or resolving the program. They cannot follow re-exports, and declaration-like text in a string can match. An empty result does not prove a symbol is absent.

Supported languages are JavaScript/TypeScript, Python, Go, Rust, Java, Ruby and SQL. Outlines read up to 2 MB and return up to 400 symbols, marking truncation. Repository searches skip generated/dependency folders and dotted directories.

What memory does to the learning half

Memory can propose concept candidates when a session failed to log its work. At the end of each turn, if the session has logged no concepts, Eklavya logs up to five candidates taken from that session’s own observations. It never touches a session that logged concepts itself.

A candidate is resolved the same way as a concept Claude logs: an existing concept with the same or a close name is reused (for example, CSRF becomes csrf); otherwise a new concept is created, such as text_truncation → text-truncation. New concepts count against max_new_concepts_per_session; candidates over that budget stay pending. domains_enabled applies. The built-in summariser only proposes concepts the graph already has; new names come from an observer model.

With an observer model, observations are written in the background, so they can arrive after the first turn ends. The next turn’s end picks them up. Until then, a session that changed code and has still logged nothing is asked at the end of the turn to log its concepts and then answer one question, so a one-turn task is not left without one.

Memory does not record quiz attempts, grant mastery, clear review debt, promote a level or open a gate. Remembered work is not assessed knowledge.

When you move or rename a checkout

History is filed under the checkout’s folder path, so a moved folder would otherwise start empty. To prevent that, the first session start in a folder records the repository’s root commits, which stay the same when the folder moves. If exactly one other project has the same root commits and its folder no longer exists, that project is the same repository before the move. Its memory, answers, level, project settings and pages are re-filed under the new folder, and the banner says so:

Memory · moved from ~/Workspace/QF · 64 entries re-filed

Nothing moves when the old folder still exists, because that is a second clone or a fork, a separate checkout. Nothing moves when more than one missing folder matches either; the banner says the history is under that many moved folders instead. A repository with no commits yet is checked again at each session start until it has one.

Root commits are recorded from this release on. For a folder moved earlier, or to choose between several matches, run eklavya memory move from the new checkout to list folders that have history but no longer exist, then eklavya memory move <old path>. Synced entries carry the new path to your other devices.

Where it lives, and turning it off

Memory and learning share ~/.eklavya/knowledge.db. Set memory.enabled: false to stop capture and recall, or memory.capture: off to stop only new capture. Set memory.retention_days to prune summarised evidence, finished jobs, receipts and the memory read log while keeping observations; the default keeps everything.

Local summarisation and retrieval make no network request about your code. Choosing an observer can send captured work to a model; optional notifications and sync can send selected data elsewhere. See Configuration and Your data for controls and backups.