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 (/skill:<name>).

Lessons, Skills

Persistent memory (MEMORY.md)

Memory entries plus MEMORY.md indexes, in the same format as Claude Code’s auto-memory. The indexes of every memory root are loaded into the workspace prompt at session start.

Cross-harness memory

memory tool

Lets the agent save a memory itself during a conversation. Enabled by default.

Cross-harness memory

gptme-util memory

CLI to list, save, audit, and recall memory entries. Usable from any harness, including Claude Code and Codex.

Cross-harness memory

gptme-util knowledge

Deprecated. Problem/resolution pairs stored as JSONL, superseded by gptme-util memory. The subgroup warns on use and will be removed in a future release.

Migrating from the knowledge base

RAG

Indexes and searches documentation, code, and past conversations with the rag tool. Available when gptme-rag is installed and [rag] is enabled in the project’s gptme.toml.

RAG

Ambient retrieval

The gptme-retrieval plugin from gptme-contrib searches your files (qmd, gptme-rag, or grep backends) before each step and injects the relevant results, each at most once per conversation.

gptme-retrieval

context_cmd

A project command whose output is added to the system prompt. It can read the first prompt from GPTME_PROMPT_INITIAL for query-dependent retrieval.

Configuration

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.

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.

Memory is not facts-only: the Claude Code format includes a feedback type for guidance a harness learns from the user (“stage explicit paths”, “don’t steal window focus”). Give such an entry keywords and it is injected when a keyword appears: gptme’s lesson auto-inclusion matches keyworded memories alongside lessons, and other harnesses get the same behaviour by running memory match (for example from a Claude Code hook; see Triggered matching). Prefer a lesson when the rule is curated and shared across workspaces; a keyworded feedback entry suits rules learned at runtime that live with a harness’s memory directory.

Migrating from the knowledge base#

gptme-util knowledge stored problem/resolution pairs in JSONL. It is deprecated: the subgroup prints a DeprecationWarning and will be removed in a future release. Memory entries cover the same job, with typed entries and recall that every harness can read.

Legacy JSONL entries are no longer injected into sessions by default. Existing knowledge save/list/search/delete commands and the stored JSONL remain available until retirement.

For compatibility, explicitly include knowledge_inject in your hook allowlist:

$ HOOK_ALLOWLIST=knowledge_inject,cli_confirm gptme

HOOK_ALLOWLIST replaces the default hook set, including mode-specific confirmation hooks. The interactive example includes cli_confirm to retain tool confirmation; add other hooks you need to the comma-separated list. Applications can include knowledge_inject in an explicit init_hooks(allowlist=[...]) call, alongside their confirmation hook (cli_confirm for interactive CLI, or server_confirm and server_elicit for server mode). New memories should use the shared Markdown memory toolkit below.

To move existing entries over:

$ gptme-util memory migrate-knowledge-jsonl

After migrating, use gptme-util memory recall QUERY in place of knowledge search, and gptme-util memory save NAME DESCRIPTION in place of knowledge save.

Lessons and memory are separate stores with separate formats, and neither loader reads the other’s directories:

  • Lessons are read recursively from lesson directories (./lessons/, ./.gptme/lessons/, ~/.config/gptme/lessons/, ~/.agents/lessons/, and [lessons] dirs in config) and use match: 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 only GPTME_MEMORY_DIRS when it is set) and use Claude Code-style frontmatter. An entry’s optional keywords make it a triggered rule for memory match (see Triggered matching); recall also uses them for ranking.

Keep them in separate directories. A memory entry with keywords covers the same ground as a lesson with match.keywords, in the memory format, so either can hold a behavioural rule. gptme’s lesson auto-inclusion matches both, with one shared session budget and dedup; set GPTME_LESSONS_INCLUDE_MEMORIES=false to match lessons only. Memory candidates match on keywords alone (no match.tools or match.patterns). There is no automatic converter: to move a lesson into memory, copy its body into an entry (typically type: feedback) and turn match.keywords into keywords; to promote a memory into a curated lesson, do the reverse. Remove the original either way, so the rule has one home.

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.

Choosing scope in the memory tool#

The conversational memory tool uses the same root selector as the CLI. Choose scope="user" for preferences or feedback that should follow you across projects; choose project for local facts or cc for the current workspace’s Claude Code memory directory. The native tool accepts name, content, and optional scope. In Markdown format:

```memory review-in-parallel --scope user
Check review feedback concurrently with CI rather than blocking on CI alone.
This preference applies across projects.
```

XML format carries the same shell-quoted arguments in its args attribute:

<tool-use>
<memory args="review-in-parallel --scope user">
Check review feedback concurrently with CI rather than blocking on CI alone.
This preference applies across projects.
</memory>
</tool-use>

In XML, --scope is reserved for scope selection. To save a literal name containing that token, quote the entire name inside the attribute, for example args="'deployment --scope user'". Other unscoped XML names retain their whole-attribute behavior; serialized literal names with --scope are quoted.

Available scopes are project, cc, agent, user, and explicit. project requires an existing workspace memory/ directory; agent requires an existing configured agent memory directory. user writes under <gptme config directory>/memory/. When GPTME_MEMORY_DIRS is set, it replaces all other roots: only explicit is available. An unavailable or invalid scope fails without falling back or creating another root. Relative paths in GPTME_MEMORY_DIRS and GPTME_AGENT_WORKSPACE are resolved against the selected workspace (the conversation workspace on the server), before directory availability checks and duplicate-root removal. Absolute paths and ~-expanded paths keep their configured destinations.

Without a scope, the tool follows the CLI default: first explicit root, else existing project memory/, else the workspace’s CC root. This is not a user-wide default, and no text classifier chooses a scope for you. Confirmation and success show the actual selected scope and destination. Interactive tool confirmation hooks receive destination metadata separately from editable memory text; editing the memory does not add that metadata to the saved entry. The selected root is made absolute before confirmation and retained throughout confirmation and saving, even if the process directory changes. The shared writer updates the index under its existing policy and budget.

Temporary Git worktrees retain their own project/CC identity under the existing workspace resolver. A project fact stays local to that worktree unless you explicitly configure a shared root; use user for cross-project feedback or a configured agent root for agent-wide knowledge. This tool does not remap a worktree to the primary checkout, migrate memories, or delete old entries.

Saving is not the same as startup delivery: gptme loads the layered indexes within its shared prompt budget, not every full entry. Claude Code’s native auto-load covers its project CC index, not an arbitrary user root; configure the recall hook below to search the other layers. Codex requires explicit recall instructions or an exported index in AGENTS.md as described below. A policy-managed entry may remain recall-only until selected for the 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, preserving unrelated hand-curated index text, and explicit index generation includes living entries with an optional one-shot truncating budget. Legacy save stages the entry and index before replacing either and rolls back an already-published entry if index publication fails. This handles ordinary write errors, not process crashes or uncooperative external writers. Existing file permission bits, POSIX ownership and Linux POSIX access ACLs are preserved; when an existing file has no access ACL, inherited staging ACLs are removed. Ownership or ACL preservation errors abort before publication. Other platform-specific ACLs are not covered by this guarantee. Atomic replacement is authorized by the parent directory: a read-only index file in a writable directory can still be replaced, just as in a managed root. Legacy save and direct pointer updates follow a symlinked MEMORY.md and atomically replace its target, leaving the link intact. The target’s parent directory authorizes that replacement. Both legacy writers hold a stable .<target-name>.memory.lock sidecar beside the resolved index target from reading through publication and rollback, so roots sharing that target serialize their updates. Writers that do not use this sidecar remain uncoordinated.

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.

Export#

export renders the store in a format suited to a specific harness:

$ gptme-util memory export --view cc > path/to/MEMORY.md
$ gptme-util memory export --view codex >> AGENTS.md

--view cc produces the same MEMORY.md index that index generates, suitable for piping directly into Claude Code’s auto-loaded file or into a hook payload.

--view codex produces a ## Memory section listing living entries with the standard recall instruction, ready to paste into an AGENTS.md file when bootstrapping a new Codex workspace. Both views respect --scope and --budget.

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 distinctive multi-word phrases and lesson-style wildcards (deploy*). Keywords match as substrings, so short single words fire inside unrelated words: ci matches “decide”, pr matches “prompt”, gate matches “navigate”. 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. By default the CLI does not remember what it already injected. Pass --dedup-session to deliver each entry at most once per session: the session_id comes from the hook payload (or --session-id for plain text), and delivered entry paths are kept in the gptme state directory, keyed by a hash of the id and pruned after a week. Wrappers that need a different policy can call gptme.memory.match.match_memories and filter_unseen themselves and apply their own 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 --dedup-session",
            "timeout": 20
          }
        ]
      }
    ]
  }
}

The hook is read-only and returns empty additionalContext when there is no relevant memory. PreToolUse fires on every tool call, so keep --dedup-session on it; without the flag an entry whose keyword keeps matching is re-injected each time. 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.