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 • OpenCode 2.x • Compatibility • Migrating from v1 • FAQ
- 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 hidden agent 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 sandboxed fork reviews only the new part of the conversation and saves what is worth keeping. Sessions closed before the fork ran are caught up at the next start.
- Auto-dream — periodic consolidation (merge / prune / rewrite) gated on time and session count, like Claude Code.
- 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. - OpenCode 1.x and 2.x — one package serves both plugin APIs (V1
server, V2setup) with the same options.
Requires OpenCode ≥ 1.18.29 (V1 plugin API) or OpenCode 2.x (@opencode/cli, tested on 2.0.22). The package exports one { id, server, setup } object: 1.x loads server, 2.x loads setup; 1.x accepts that dual object only from 1.18.29 on.
// OpenCode 2.x — opencode.json
{
"plugins": [{ "package": "opencode-claude-memory" }]
}OpenCode 2.x also accepts (and normalises) the 1.x plugin key.
That's it. Start opencode and use it as usual. Memories live in ~/.claude/projects/<project>/memory/ (or under $CLAUDE_CONFIG_DIR), exactly where Claude Code keeps them.
graph LR
U[User turn] --> R[Hidden recall agent<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 fork<br/>new messages only]
E --> M[(~/.claude/projects/<project>/memory/)]
E --> D{Auto-dream gate}
D -->|24h & 5 sessions| C[Consolidation fork]
C --> M
The hook names below are OpenCode 1.x's; on 2.x the same steps run on the V2 plugin API, with the differences listed in Differences on OpenCode 2.x.
- Recall —
experimental.chat.messages.transformstarts a selector prefetch for each new user turn (a hidden child session runningopencode-memory-recall).experimental.chat.system.transformwaits for it up torecall.waitMs(default 1.5 s) and injects the selected memories. Memories already in the conversation are not re-injected; after compaction they can surface again. - Extraction — every
session.idleis debounced (extract.debounceMs). The plugin fetches the session's messages, slices them after the per-session watermark, and — only if there is a new user message — runsopencode-memory-extractin a child session restricted tomemory_save/memory_list/memory_read. On success the watermark advances; if the main agent already saved memory in that stretch the fork is skipped. - Catch-up — on start-up the plugin lists the project's sessions and extracts the ones updated after their watermark (at most
extract.catchUpLimit). This covers "answer, then quit immediately". - Auto-dream — after each extracted session the gate is evaluated (
autodream.minHourssince the last pass andautodream.minSessionsextracted since). When it passes,opencode-memory-dreamruns with all five memory tools. 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 and, on OpenCode 2.x, the log file) 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. The plugin options are identical on OpenCode 1.x and 2.x; only the surrounding keys differ.
// opencode.json — OpenCode 1.x
{
"plugin": [
["opencode-claude-memory", {
"extract": { "enabled": true, "timeoutMs": 120000, "debounceMs": 10000, "maxConversationChars": 60000, "catchUpLimit": 5 },
"autodream": { "enabled": true, "minHours": 24, "minSessions": 5, "timeoutMs": 300000 },
"recall": { "enabled": true, "waitMs": 1500, "timeoutMs": 30000, "maxMemories": 5 }
}]
],
"agent": {
"opencode-memory-extract": { "model": "anthropic/claude-haiku-4-5", "steps": 20 },
"opencode-memory-recall": { "model": "anthropic/claude-haiku-4-5" },
"opencode-memory-dream": { "model": "anthropic/claude-sonnet-5" }
}
}- Every option above is optional; the values shown are the defaults. Unknown keys are rejected when the plugin loads.
- When the same plugin is listed in both the global and the project
opencode.json, OpenCode keeps the last declaration (project wins); options are not merged across files. - The three agents are registered hidden with a memory-only tool sandbox. On 1.x, override any field (
model,steps,temperature, …) underagent.<name>; the plugin fills in the rest. CLAUDE_CONFIG_DIRis honoured exactly like Claude Code does, and is the only environment variable the plugin reads.
// opencode.json — OpenCode 2.x
{
"plugins": [
{ "package": "opencode-claude-memory", "options": { "extract": { "debounceMs": 10000 } } }
],
"agents": {
"opencode-memory-extract": {
"model": "opencode/gpt-6-luna",
"steps": 20,
"permissions": [{ "action": "webfetch", "resource": "*", "effect": "allow" }]
}
}
}optionstakes the same keys as the 1.x example above.- Agent overrides live under
agents.<name>and use 2.x agent fields (model,steps,permissions, …). The plugin's sandbox rules (*deny, then allow for the agent's memory tools) come first and your ownpermissionsafter them; 2.x applies the last matching rule, so your rule wins. - The main agent needs no extra permission on 2.x's default
buildagent, which allows everything. If you restrict permissions (for example*→askordeny), add{ "action": "memory_*", "resource": "*", "effect": "allow" }to yourpermissionsso the memory tools are neither blocked nor prompted for.
Models for the memory agents. OpenCode Zen's free models (e.g. opencode/big-pickle) reject every request whose tool list lacks OpenCode's shell tool (HTTP 403, "OpenCode's free tier can only be used from within OpenCode"). The sandboxed extraction and auto-dream forks are exactly such requests on both 1.x and 2.x, and so is recall on 2.x. On a free Zen model, extraction, auto-dream and (on 2.x) recall therefore fail. Configure a paid model for opencode-memory-extract, opencode-memory-dream and opencode-memory-recall (e.g. opencode/gpt-6-luna); the main agent can stay on a free model.
Logs. On 1.x logs go to the OpenCode service log (opencode log directory, service opencode-claude-memory). 2.x gives plugins no log API, so the plugin writes <CLAUDE_CONFIG_DIR>/opencode-memory/<project>/opencode-memory.log instead (rotated at about 1 MB).
The V2 plugin API exposes less than V1, so a few behaviours differ:
- Extraction only sees messages after the last compaction. V2 plugins cannot read the full message history. If the extraction watermark was compacted away, extraction restarts from the start of the remaining context; anything before it that was not extracted yet is lost (a warning is logged).
- Catch-up only covers known sessions. V2 plugins cannot list sessions, so start-up catch-up only covers sessions already recorded in
extraction-state.json. A session created and finished while the plugin was not running is not caught up. - One sandbox layer instead of two. Forks are restricted by the agents' permission rules (also passed to the fork as session permissions); V2 has no per-prompt tool map.
- Recall is a one-shot
generate.textcall instead of a child session with structured output. The selector's answer is parsed as JSON text, and an answer that does not parse recalls nothing. The recall agent'smodelis used if set; temperature is not configurable. - Logs go to a file in the plugin state directory, not to the OpenCode log (see Logs).
- Free Zen models break recall too, not only extraction and auto-dream (see Models for the memory agents).
| 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 (findRelevantMemories.ts port): a hidden child session on 1.x, a generate.text call on 2.x |
| Extraction / auto-dream | after session, gated | after session.idle + start-up catch-up, 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.v2 removes the shell wrapper, the opencode-memory CLI and every OPENCODE_MEMORY_* environment variable. Memory files are untouched and need no conversion.
# 1. remove the v1 shell hook, then the v1 package (v2 no longer needs a global install)
opencode-memory uninstall # or delete the ">>> opencode-memory auto-initialization >>>" block from your rc file
npm uninstall -g opencode-claude-memory
# 2. drop OPENCODE_MEMORY_* from your shell configuration
grep -n OPENCODE_MEMORY ~/.zshrc ~/.bashrc ~/.zshenv ~/.profile 2>/dev/null// 3. pin the major in opencode.json — OpenCode caches npm plugins per specifier,
// so a bare "opencode-claude-memory" keeps serving the v1 it installed earlier
{
"plugin": ["opencode-claude-memory@2"]
}Everything the environment variables used to control now lives under extract, autodream, recall and agent.opencode-memory-* in opencode.json — see Configuration. The v1 documentation, including the full list of environment variables, stays available 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 the extraction fork see my whole conversation? Only the messages after the last extraction, capped at extract.maxConversationChars (newest first); on OpenCode 2.x only those after the last compaction. The fork can only call memory tools.
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/To try an unpublished build, run bun run build and point OpenCode at the checkout:
// OpenCode 1.x — reads package.json "main" (dist/index.js)
{ "plugin": [["file:///abs/path/to/opencode-claude-memory", { "extract": { "debounceMs": 10000 } }]] }// OpenCode 2.x — resolves a directory as <dir>/server or <dir>/index, not via package.json
{ "plugins": [{ "package": "file:///abs/path/to/opencode-claude-memory/dist" }] }Releases are cut by semantic-release on push to main.