Memory#
gptme can carry context across conversations in several complementary ways: some you curate yourself, some the agent writes, and some are retrieved automatically based on what you are working on.
System |
What it is and when it is loaded |
Read more |
|---|---|---|
Lessons and skills |
Curated Markdown guidance. Lessons are included automatically when their
keywords, patterns, or tools match; skills are loaded by name or invoked
as slash commands ( |
|
Persistent memory ( |
Memory entries plus |
|
|
Lets the agent save a memory itself during a conversation. Enabled by default. |
|
|
CLI to list, save, audit, and recall memory entries. Usable from any harness, including Claude Code and Codex. |
|
|
Problem/resolution pairs stored as JSONL. Matching entries are injected once at the start of a conversation when the first prompt has enough keywords to search. |
|
RAG |
Indexes and searches documentation, code, and past conversations with
the |
|
Ambient retrieval |
The |
|
|
A project command whose output is added to the system prompt. It can
read the first prompt from |
Memory, lessons, or skills?#
These systems overlap, but each has a distinct job:
Lessons are curated guidance that shapes how the agent works. They are written and reviewed ahead of time, usually versioned in a repository, and included automatically when their
match:keywords, patterns, or tools come up in a conversation.Skills are named workflows, loaded when a task calls for them or invoked directly as slash commands.
Memory holds facts learned while working: user preferences, decisions and their rationale, and project facts that are not obvious from the code. Agents and harnesses write it at runtime; the index is always loaded, and full entries are found with
recall.The knowledge base (
gptme-util knowledge) holds problem/resolution pairs, so a fix found once can resurface when a similar problem comes up.
Rule of thumb: if it tells the agent how to behave, write a lesson; if it is a procedure to run on request, write a skill; if it records what is true about a user or project, save a memory.
Lessons and memory are separate stores, and neither reads the other:
Lessons are read recursively from lesson directories (
./lessons/,./.gptme/lessons/,~/.config/gptme/lessons/,~/.agents/lessons/, and[lessons] dirsin config) and usematch:frontmatter.Memory entries are read from the top level of each memory root (
./memory/, the Claude Code project memory directory,$GPTME_AGENT_WORKSPACE/memory/, and~/.config/gptme/memory/, or onlyGPTME_MEMORY_DIRSwhen it is set) and use Claude Code-style frontmatter. An entry’s optionalkeywordsfield only affectsrecallranking.
Keep them in separate directories. There is no tooling that converts between them; if a memory turns out to be lasting guidance, rewrite it as a lesson and remove the memory entry.
Cross-harness memory#
gptme-util memory provides one local, Markdown-based memory store that can
be shared by gptme, Claude Code, Codex, and other agent harnesses. It is
modeled on Claude Code’s auto-memory: entries use Claude Code-compatible
frontmatter and are read from layered roots (project, Claude Code, agent, and
user); the nearest entry wins when names collide.
Inspect and write memory#
$ gptme-util memory roots
$ gptme-util memory list
$ gptme-util memory show prefer-short-answers
$ gptme-util memory save prefer-short-answers \
"User prefers short, direct answers." --type feedback < details.md
$ gptme-util memory index
index prints by default. Pass --write only when you want to replace the
selected root’s MEMORY.md with a generated index.
Persistent index selection and budget#
A large memory root can keep a small, curated always-on view without removing
living entries from recall. To opt in, create .memory-index.json inside
that memory directory:
{
"version": 1,
"budget": 21000,
"selected": ["prefer-short-answers.md", "review-policy.md"]
}
selected contains unique local filenames, not entry names. Every selected
file must exist and be living. The generated view retains type/name grouping;
the selection list is not a display ordering. Entries omitted from this list
remain available to recall, list, and show. An empty list produces
only the index header.
index (print, --write, and --check), save, and supersede all
use the persistent UTF-8 byte budget, including headers and newlines. Required
entries are never silently dropped: an oversized view fails before changing
entries, policy, or index. --budget can tighten the stored cap for one
invocation, but cannot raise it. Edit the policy to change the persistent cap.
In a managed root, save regenerates the selected view. New entries stay
unselected until explicitly added to the policy. Updating an entry preserves
its lifecycle, provenance, and omitted title/type/metadata; supplied description
and body replace the previous values. Updating an existing entry requires valid
frontmatter, including in a legacy root. Lenient parsing remains available for
reads, but writes refuse malformed YAML instead of silently discarding fields
while attempting a repair. Correct that file’s frontmatter before saving again.
supersede transfers a selected old
filename to its replacement, deduplicating the list, and commits both entries,
the policy, and the view together. Ordinary write failures roll back these
replacements; this is not a crash-recovery transaction log.
Before migrating a hand-curated index, copy its link text into entry title
and its instructions into description. Validate a copy with index and
review both link coverage and wording before index --write. Preserve any
archive as history. Direct file writers and harness hooks must adopt the same
policy; creating the policy does not intercept tools that edit files themselves.
When editing policy by hand, pause concurrent memory writers for that root.
Roots without a policy retain legacy behavior: save upserts one pointer
line, and explicit index generation includes living entries with an optional
one-shot truncating budget.
Supersession and audit#
Replace an obsolete entry with an already-existing living entry, then check the root’s strict YAML and bidirectional supersession links:
$ gptme-util memory supersede old-belief new-belief
$ gptme-util memory audit
$ gptme-util memory audit --quiet
supersede updates both entries (superseded_by on the old entry and
supersedes on the replacement) and regenerates the selected root’s index.
Both entries must be in the same root; use --scope when needed. The command
refuses malformed YAML and invalid field types rather than rewriting them
through the lenient read fallback. audit exits non-zero for malformed
entries, duplicate names, dangling targets, or asymmetric links, making
audit --quiet suitable for lifecycle hooks.
Recall#
Recall searches all living entries across the layered roots:
$ gptme-util memory recall "How should private code be reviewed?" -k 3
$ gptme-util memory recall "exact identifier" --format json
With gptme-rag and its lexical extra installed, --backend auto uses
its TF-IDF index. A minimal gptme installation falls back to a stdlib
token-overlap scorer. Output always reports the backend that actually ran; use
--backend tfidf when fallback would be unacceptable.
Triggered matching#
Use keyword triggers when a memory must arrive on this turn, not only in the
session-start index. MEMORY.md is a budgeted always-on view; triggered
matching is for the smaller set of facts that should interrupt the current
action (a release checklist, a private-review rule, a recurring failure mode).
When to save a trigger phrase. Attach --keyword as you write the memory,
using the phrase you will actually see in a prompt or tool call. Prefer short,
distinctive phrases and lesson-style wildcards (deploy*). Names,
descriptions, and bodies never trigger, so a memory named release-rule will
stay silent until you give it keywords. Skip keywords on ambient facts that
already belong in the index.
$ gptme-util memory save release-rule "Verify releases before declaring done" --keyword "stable release" --keyword "deploy*" < rule.md
$ gptme-util memory match "Cut the stable release" --format json
Repeated --keyword options replace the entry’s phrases; omit the option to
keep them. From Python, keywords=[] clears them. Saving still follows the
index policy and budget; matching is read-only.
How hooks give you the memory in time. Put memory match on
PreToolUse so a pending tool call can still pull the matching rule before
it runs. Keep memory recall on UserPromptSubmit for broader search at
the start of a turn (see the hook configuration below). --prompt - reads the
Claude Code JSON event from stdin: the prompt for UserPromptSubmit, every
string under tool_input for PreToolUse (nested dicts and lists
included). Outer envelope fields (session_id, transcript_path,
cwd, tool_name) never match; nested keys inside tool_input that
reuse those names are tool arguments and do. --format hook-json returns
empty additionalContext on no match so the hook stays silent.
Dedup and budget belong to the wrapper. The CLI does not remember what it
already injected. Call gptme.memory.match.match_memories from the harness
and apply your own session dedup and injection budget to the returned entries,
scores, and matched_by reasons. -k (default 5) and --body-chars
(default 1200 Unicode code points) are hard caps, not a substitute for that.
Matching uses the same layered-root precedence as the rest of the store:
superseded and historical entries stay excluded, a nearer entry of any status
occupies the name, and index selection does not restrict who can trigger.
Keywords are case-insensitive substrings; * matches word characters, never
spaces or punctuation. Empty keywords and a lone * are disabled. Each
matching keyword scores one point. Equal scores keep name order.
Claude Code hook#
Add the command below to a UserPromptSubmit hook in
.claude/settings.json. It reads Claude Code’s JSON event from stdin and
returns a valid additionalContext response:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "gptme-util memory recall --prompt - --format hook-json",
"timeout": 20
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "gptme-util memory match --prompt - --format hook-json",
"timeout": 20
}
]
}
]
}
}
The hook is read-only and returns empty additionalContext when there is no
relevant memory. Keep the executable on Claude Code’s hook PATH; if the
workspace uses an isolated environment, call its absolute gptme-util path.
Codex / AGENTS.md integration#
Codex has no hook mechanism, so memory access is driven by instructions in
AGENTS.md. The gptme repository’s own AGENTS.md already contains a
## Memory section; copy or adapt it for any workspace that runs Codex
sessions.
The two key patterns for Codex agents:
Recall at session start — surface memories relevant to the current task:
gptme-util memory recall "<one-line task description>" -k 5
Save a memory — persist something worth keeping across sessions:
gptme-util memory save <slug> "<one-line description>" --type <type> <<'EOF'
<body>
EOF
Valid --type values match Claude Code’s memory taxonomy: user,
feedback, project, reference.
The entry is written to project memory/ when that directory exists,
otherwise to ~/.claude/projects/<workspace-hash>/memory/. The selector
checks existence, not writability: an unwritable project directory makes
save fail rather than falling back.
gptme-util memory recall (and the Claude Code hook) search every
layered root, so other harnesses see the entry on their next recall.
gptme’s session-start workspace prompt also loads the indexes of every
existing root, within a shared 64 KB budget. Claude Code’s native
MEMORY.md auto-load reads only the Claude Code root, so pass
--scope cc when the memory must appear in Claude Code without running
recall.
Writer provenance#
Harness wrappers can preserve their session identity through the shared writer:
gptme-util memory save decision "Reviewed decision" --type reference \
--metadata '{"originSessionId":"session-123","node_type":"memory"}' < body.md
--metadata accepts a JSON object and merges its keys into existing entry
metadata. Omitted keys survive updates; lifecycle fields and root index policy
remain governed by the store. The reserved type key must use --type
instead. Invalid JSON, a non-object, or a reserved key fails before writing.