Persistent, local-first shared memory for OpenCode and Claude Code - one plugin, zero migration.
This OpenCode plugin lets OpenCode read and write Claude Code-compatible Markdown memory files, so both CLIs share the same project context.
Claude Code writes memory β OpenCode reads it. OpenCode writes memory β Claude Code reads it.
Quick Start β’ How it works β’ Configuration β’ Compatibility β’ Migrating from v1 β’ FAQ
This is a fork of kuitos/opencode-claude-memory for folders that other tools also write to, such as dsh-unified-memory or Claude Code itself. Upstream rewrites a memory's whole file on every save, which drops any frontmatter it does not know, including other tools' provenance. This fork:
-
Keeps what it does not own. Saving an existing memory changes only its name, description, type,
modifiedand body; every other frontmatter line, including another tool'smetadata.origin, is kept byte for byte. -
Records provenance. Files it creates carry
metadata.origin: opencodeandmetadata.modified; edits to files another tool created addmetadata.updatedBy: opencode. -
Keeps a copy of other tools' memories it deletes. Deleting a memory it did not create (for example during auto-dream consolidation) first copies it to
$CLAUDE_CONFIG_DIR/opencode-memory/<project>/trash/<timestamp>/. -
Writes atomically. Memory files and
MEMORY.mdare written to a temporary file and renamed into place. -
Quotes YAML values that need it, and reads quoted values back correctly.
-
Targets OpenCode 2. OpenCode 2 replaced the plugin API, so
mainis a port to it (Plugin.define, JSON-schema tools, thecontextsession hook,ctx.generate.text). The OpenCode 1 version of this fork is kept on theopencode-1branch; upstream itself still targets OpenCode 1.
The fork is not published to npm, so it is installed from a clone (see Quick Start).
- Memory tools -
memory_save/memory_delete/memory_list/memory_search/memory_read, plus the Claude Code memory instructions injected into every system prompt. - LLM recall - before each turn a small model call picks the memories relevant to the query; they appear in the first LLM call, including single-step questions.
- Automatic extraction - after a session goes idle, a model call reviews only the new part of the conversation and the plugin saves what is worth keeping.
- Auto-dream - periodic consolidation (merge / prune / rewrite) gated on time and session count, like Claude Code, with a backup of the memory directory before every pass.
- Claude Code-compatible - same directory, same file format, same taxonomy, same worktree handling.
MEMORY.mdis edited line by line so hand-organised indexes stay intact. - Cross-platform, no shell hook - everything runs inside the OpenCode process through the plugin SDK. No
python3, nojq, no wrapper. - Nothing left behind - background work uses stateless
ctx.generate.textcalls, so it never creates sessions in your session list.
Requires OpenCode β₯ 2.0, Bun and git.
curl -fsSL https://raw.githubusercontent.com/mattcarvercom/opencode-claude-memory/main/scripts/install.sh | bashThe script clones the fork into ~/.local/share/opencode-claude-memory (--dir to change it), builds it, and points your global opencode config at the checkout with a file:// entry in plugins, replacing any opencode-claude-memory entry (also one in the OpenCode 1 plugin list) and keeping its options. Run it again to update. --config FILE edits another config file, and --no-config leaves configuration to you; when piping, pass options after bash -s --, as in ... | bash -s -- --no-config. A config file with comments is edited only where the plugin's name appears; if there is none to replace, the script prints the line to add.
To do the same by hand:
git clone https://github.com/mattcarvercom/opencode-claude-memory ~/.local/share/opencode-claude-memory
cd ~/.local/share/opencode-claude-memory && bun install && bun run buildOpenCode 2 loads a plugin directory through its root index.js, which re-exports the build in dist/. After pulling changes, run bun install && bun run build again (dist/ is not committed), then opencode service restart or restart opencode.
Set model. The plugin's background calls (recall, extraction, auto-dream) are plain model calls outside any session. Without model they use OpenCode's default model, which fails when that is one of the free models that only work inside a session. Pick a small, cheap model you have a key for, for example deepseek/deepseek-flash.
Memories live in ~/.claude/projects/<project>/memory/ (or under $CLAUDE_CONFIG_DIR), exactly where Claude Code keeps them.
graph LR
U[User turn] --> R[Recall call<br/>selects relevant memories]
R --> S[System prompt: instructions + MEMORY.md + recalled memories]
S --> A[Main agent answers<br/>memory_* tools available]
A --> I[session.idle]
I --> E[Extraction call<br/>new messages only]
E --> M[(~/.claude/projects/<project>/memory/)]
E --> D{Auto-dream gate}
D -->|24h & 5 sessions| C[Consolidation call]
C --> M
- Recall - the
contextsession hook runs before every model call. On the first call of a user turn it starts a selector (onectx.generate.textcall over the memory manifest) and waits for it up torecall.waitMs(default 1.5 s), then appends the selected memories to the system prompt. Later calls of the same turn reuse the result. If the selector is slower thanwaitMs, its result appears from the next call on. - Extraction - every
session.idle(and finished execution) is debounced (extract.debounceMs). The plugin reads the session's messages, slices them after the per-session watermark and, only if there is a new user message, asks the model for memories worth keeping as a JSON list. The plugin validates the list and writes the files itself; extraction only creates memories and never overwrites an existing one. On success the watermark advances; if the main agent already saved memory in that stretch the call is skipped. Only top-level sessions of the plugin's own directory are extracted. - Auto-dream - after each extracted session the gate is evaluated (
autodream.minHourssince the last pass andautodream.minSessionsextracted since). When it passes, the model gets every memory in full and answers with files to save and files to delete. The memory directory is copied to<state>/dream-backups/first (newest 3 kept), deleting more than half of the memories in one pass is refused, and memories another tool created are copied to the trash before deletion. A lock file prevents two OpenCode processes from consolidating at once. - Ignore memory - "ignore memory" in a user message switches memory off for the rest of the session (no index, no recall); "use memory again" switches it back on.
State that is private to the plugin (watermarks, auto-dream gate, lock, backups, trash, plugin.log) lives in <CLAUDE_CONFIG_DIR>/opencode-memory/<project>/, never inside the Claude Code project directory.
All behaviour is configured through OpenCode's own configuration. There are no OPENCODE_MEMORY_* environment variables.
// opencode.json
{
"plugins": [
{
"package": "file:///home/you/.local/share/opencode-claude-memory",
"options": {
"model": "deepseek/deepseek-flash",
"extract": { "enabled": true, "timeoutMs": 120000, "debounceMs": 10000, "maxConversationChars": 60000 },
"autodream": { "enabled": true, "minHours": 24, "minSessions": 5, "timeoutMs": 300000, "model": "deepseek/deepseek-pro" },
"recall": { "enabled": true, "waitMs": 1500, "timeoutMs": 30000, "maxMemories": 5 }
}
}
]
}- Every option is optional; the numbers shown are the defaults. Unknown keys are rejected when the plugin loads.
model(provider/model, the spellingopencode run --modeltakes) is used for all three background tasks;extract.model,autodream.modelandrecall.modeloverride it per task. Without any of them OpenCode's default model is used.CLAUDE_CONFIG_DIRis honoured exactly like Claude Code does, and is the only environment variable the plugin reads.
OpenCode 2 gives plugins no log channel, so the plugin writes its own JSON-lines log to <CLAUDE_CONFIG_DIR>/opencode-memory/<project>/plugin.log (truncated at 512 KB). Failed extractions and consolidations are logged there.
| Aspect | Claude Code | This plugin |
|---|---|---|
| Memory directory | ~/.claude/projects/<sanitized canonical git root>/memory/ |
identical (sanitizePath, worktree β main repo resolution ported byte for byte) |
| File format | Markdown + name / description / type frontmatter |
identical; frontmatter parsed only within the first 30 lines, as in Claude Code |
| Taxonomy | user, feedback, project, reference |
identical |
MEMORY.md |
one-line pointers, hand-organisable | read with the same truncation rules; written with minimal line-level edits |
| Sub-directories | team/x.md etc. |
scanned, recalled and addressable from every tool |
| System prompt | memory instructions + index + recalled memories | ported sections (memoryTypes.ts, memdir.ts) |
| Recall | LLM side query | LLM side query as a stateless model call (findRelevantMemories.ts port) |
| Extraction / auto-dream | after session, gated | after session.idle, gated the same way |
Memory files written by either tool need no conversion in either direction.
---
name: User prefers terse responses
description: User wants concise answers without trailing summaries
type: feedback
---
Skip post-action summaries. User reads diffs directly.
**Why:** User explicitly requested terse output style.
**How to apply:** Don't summarize changes at the end of responses.OpenCode 2 does not run OpenCode 1 plugins, so this fork's main replaces the OpenCode 1 build. Memory files are untouched and need no conversion.
- Config:
pluginbecomesplugins, and["name", { options }]becomes{ "package": "name", "options": { ... } }. OpenCode 2 still reads the oldpluginkey, and the install script moves the entry for you. - Agents are gone. The three hidden
opencode-memory-*agents (and theiragent.*overrides) no longer exist; setmodel,recall.model,extract.modelorautodream.modelinstead. - Extraction only creates. It no longer re-saves an existing memory with merged content; updating existing memories is left to the agent and to auto-dream.
- No start-up catch-up.
extract.catchUpLimitis removed. OpenCode 2 keeps a background server running after the terminal closes, so the debounce timer survives quitting. - Recalled memories are not de-duplicated across turns. The system prompt is rebuilt for every model call, so a relevant memory is selected again on a later turn.
Stay on OpenCode 1? Use the opencode-1 branch of this fork, or upstream. The even older shell-hook version is documented in the v1 README.
Is this a new memory system? No. It is a compatibility layer around Claude Code's memory layout and conventions.
Do I need to migrate existing memory? No. Existing Claude Code memory files are used as they are.
Where is data stored? ~/.claude/projects/<project>/memory/ (or $CLAUDE_CONFIG_DIR/projects/...). Plugin state lives in $CLAUDE_CONFIG_DIR/opencode-memory/<project>/. Memories the plugin deletes that are not purely its own (Claude Code's, another tool's, or one of its own that another tool has since edited) are copied to trash/<timestamp>/ there first.
Can I disable extraction, auto-dream or recall? Yes - extract.enabled, autodream.enabled, recall.enabled in the plugin options.
Why did my first answer take a moment longer? The system prompt waits up to recall.waitMs for the selector. Set it to 0 to never wait (recalled memories then appear from the second LLM call of a turn onwards).
Does extraction see my whole conversation? Only the messages after the last extraction, capped at extract.maxConversationChars (newest first). The model cannot call tools or touch files; it returns a list and the plugin validates and writes it.
Recall, extraction or auto-dream never seem to run. Check plugin.log (see Configuration). The usual cause is a default model that cannot be called outside a session; set model.
bun install
bun test # unit, integration and eval tests
bun run evals # readable task-eval report
bun run lint # biome
bun run typecheck
bun run build # emits dist/Upstream cuts releases with semantic-release on push to main; that workflow is disabled in this fork, which is not published to npm.