MeMesh — local collaboration for AI coding agents. Share memory across sessions, exchange task messages, and preserve work context. One SQLite file; no cloud…

Shared local memory for Claude Code and Codex.
MeMesh helps your coding agents remember your preferences, project decisions, and lessons from past work, so you spend less time explaining the same things in every new session.
Memories live in one file on your computer: ~/.memesh/knowledge-graph.db. MeMesh needs no cloud memory service or separate AI model. Automatic reminders depend on the host integration; a handoff is context, not a guarantee that a task will restart itself.
Claude Code — run these inside Claude Code, then restart it:
/plugin marketplace add PCIRCLE-AI/memesh
/plugin install memesh@pcircle-memesh
Codex — run these in your terminal, then restart Codex:
codex plugin marketplace add PCIRCLE-AI/memesh
codex plugin add memesh@pcircle-memesh
To use the command-line tools too, install Node.js 22.13+, then run:
npm install -g @pcircle/memesh
memesh doctor
Installation and update guide · Other integrations
Ask your agent in plain language:
Remember that this project uses SQLite.
What did we decide about login last time?
Save where we left off so the next session can pick it up.
Claude Code's plugin provides automatic capture and reminders. The Codex plugin can provide context at session start once its hooks are allowed to run; other capture behavior varies by host. You can also ask the agent to store or look up memories directly.
Claude Code inbox reminders work without Channel setup: hooks use the local claude-code recipient unless you configured another identity. They check at startup, your next prompt and turn end, and stop reminding once the agent records intake for that message. Live Channel push is optional; see the messaging guide.
Ordinary Claude Code MCP sessions register in message discover without enabling Channel. Online means the session is connected; immediate push still requires Channel. Without it, use the durable inbox and normal hook reminders.
Unreleased source builds add /memesh-memories in Claude Code to view and edit project memories. Claude Desktop edits inside the pane; terminal Claude Code and Codex use a native form. In Codex, ask $memesh to find and edit a memory. Fill only the title or paragraphs you want to change, leaving other fields blank. Save reads back the stored result; Cancel saves nothing. Unsupported hosts keep the existing conversation workflow.
Full tool and command reference · Agent messaging · How it works · Contributing
| Tool | What it does |
|---|---|
remember | Save a memory; decisions need a reason. |
recall | Find memories. |
forget | Archive a memory or remove one observation. |
learn | Save a mistake, its cause, and how to avoid it. |
briefing | Read the project's current memory context. |
task_state | Read or record the goal, next step, blocker, and done work. |
message | Find running local agents and exchange messages. |
export | Back up memories as JSON. |
import | Import a memory backup. |
work_package | Prepare a bounded memory digest or transcript package for human review. |
user_patterns | Summarize work patterns. |
improvement | Propose an improvement for human review. |
Plugins ship 10 hooks. The intent reminder recognizes "remember this" (5 languages). Memory editing lives in Claude Code/Claude Desktop Code through the MeMesh Mod, and in Codex through MCP/native forms. There is no standalone Web Dashboard.
Scored Ranking — relevance (30%) + recency (25%) + frequency (18%) + confidence (17%) + recall impact (10%). See the architecture for details.
Scored Ranking — Results ranked by relevance (30%) + recency (25%) + frequency (18%) + confidence (17%) + recall impact (10%).
Agent messaging, the exact rules (full guide: docs/platforms/agent-messaging.md):
agent setup is required. SessionStart launches an owner-private companion. SessionEnd keeps a bounded 45-second idle queue window, resume replaces the prior exact generation, and expiry removes the registration. A message accepted during that idle window reaches the thread as a short notice when the same thread resumes, and the agent then fetches the body from the inbox; it is not a claim that a stopped UI was awakened. memesh agent setup codex-session remains available only when one workspace needs a stable named principal. The complete native envelope, including routing metadata and payload, is capped separately at 16,384 bytes (16 KiB). An exact-session send returns success only after that native queue accepts it; an oversized full envelope reports native_message_too_large, an unreachable local router reports router_unreachable, and other unavailable or rejected sessions report recipient_unavailable. Scoped recovery data remains durable for all sender-side and recipient-side failures. Principal targets retain durable store-and-forward behavior. Native acceptance is not acknowledgement or disposition, and native messages must contain no secrets.memesh message storage report shows what is stored. Native delivery works on macOS and Linux only.message discover; this is an evidence boundary, not a claim that those hosts are universally incompatible.--project value copied between the two: each host derives its own routing project from its own working directory (Claude) or --workspace (Codex), so starting both in the same repository lands them in the same project automatically.hooks/native-memory.js 171 lines1// Claude Code Mod. Rendering and MCP calls stay inside the existing host; no model turn or web server.
2const SERVER = 'plugin:memesh:memesh';
3const PANE = 'memesh-memory';
4// Left out, a pane gets a third of the terminal, which cuts a memory's text off below the actions.
5const OPEN = { id: PANE, title: 'MeMesh memories', focus: true, closeOnEscape: true, rows: 24 };
6
7// Words a person reads in place of the stored identifiers; anything unlisted is shown as stored.
8const TYPE_NAMES = { decision: 'Decision', note: 'Note', lesson_learned: 'Lesson', pattern: 'Pattern', bug: 'Bug', 'task-state': 'Task state' };
9const HOST_NAMES = { 'claude-code': 'Claude Code', codex: 'Codex', 'codex-mcp-client': 'Codex', cli: 'MeMesh CLI' };
10
11let query = '';
12let project;
13let folder;
14let memories = [];
15let selected;
16let draft;
17let busy = false;
18// tone is undefined (plain), 'success' or 'error'; the words carry the meaning, colour only confirms it.
19let notice = { text: '' };
20
21const call = async ($, tool, args) => {
22 const result = await $.mcp.call(SERVER, tool, args);
23 const text = result.content?.find(item => item.type === 'text')?.text;
24 if (result.isError) throw new Error(text || 'MeMesh could not complete this operation.');
25 return JSON.parse(text);
26};
27const run = async ($, action) => {
28 if (busy) return;
29 busy = true;
30 notice = { text: '' };
31 $.ui.invalidate('ui.render');
32 try { await action(); }
33 catch (error) {
34 // A dropped reply may come after a commit, so promise nothing about the data: point to where to check, then the reason.
35 const reason = (error instanceof Error ? error.message : '').replace(/\s+/g, ' ').trim();
36 notice = { text: 'This did not finish. Reopen the memory to check what it holds now.', tone: 'error', detail: reason && `Reason: ${reason.length > 90 ? `${reason.slice(0, 89)}…` : reason}` };
37 }
38 finally { busy = false; $.ui.invalidate('ui.render'); }
39};
40const search = ($, value) => run($, async () => {
41 query = value;
42 selected = undefined;
43 draft = undefined;
44 const result = await call($, 'recall', { query, limit: 5 });
45 project = result.scope?.project;
46 // The first slice edits only the current project, never an implicitly selected global memory.
47 memories = project ? result.entities.filter(memory => memory.projects?.includes(project)) : [];
48 if (!project) notice = { text: 'This folder has no MeMesh project, so there are no memories to show. Open Claude Code in your project folder.', tone: 'error' };
49 else if (!memories.length) notice = { text: query ? 'No memories match. Try other words.' : 'This project has no memories yet.' };
50});
51const edit = $ => run($, async () => {
52 try {
53 const result = await call($, 'remember', { name: selected.name, project, interactive: true });
54 if (result.status === 'saved') {
55 selected = { ...selected, title: result.memory.title, observations: [result.memory.body], truncated: undefined };
56 memories = memories.map(memory => memory.name === selected.name ? selected : memory);
57 notice = { text: '✓ Saved. Below is the text read back from the database.', tone: 'success' };
58 } else if (result.status === 'cancelled') notice = { text: 'Cancelled. Nothing was changed.' };
59 else notice = { text: 'Nothing changed, so nothing was saved.' };
60 } finally {
61 // Closing the form hands the keys to the prompt, whatever it ended in; ask for them back for the pane.
62 await $.ui.open(OPEN);
63 }
64});
65const editInPane = $ => run($, async () => {
66 const result = await call($, 'remember', { name: selected.name, project, editor: { action: 'prepare' } });
67 selected = { ...selected, ...result.memory, truncated: undefined };
68 draft = { revision: result.revision, memory: result.memory, title: '', observations: result.memory.observations.map(() => '') };
69});
70const saveInPane = $ => run($, async () => {
71 const { memory, revision, title, observations } = draft;
72 const result = await call($, 'remember', { name: memory.name, project, editor: {
73 action: 'save', revision,
74 title: title.trim() ? title : memory.title,
75 observations: observations.map((text, index) => text.trim() ? text : memory.observations[index]),
76 } });
77 selected = { ...selected, ...result.memory, truncated: undefined };
78 memories = memories.map(memory => memory.name === selected.name ? selected : memory);
79 draft = undefined;
80 notice = result.status === 'saved'
81 ? { text: '✓ Saved. Below is the text read back from the database.', tone: 'success' }
82 : { text: 'Nothing changed, so nothing was saved.' };
83});
84
85export function register(on) {
86 on('session.start', async ($, event, next) => {
87 // The plugin's skill already owns /memesh (as /memesh:memesh), so the pane takes its own name.
88 await $.command.register({ name: 'memesh-memories', description: 'View and edit this project’s MeMesh memories' });
89 return next(event);
90 });
91 on('command.run', { command: 'memesh-memories' }, async ($, event) => {
92 try {
93 folder = await $.session.cwd();
94 await $.ui.open(OPEN);
95 } catch (error) {
96 return { text: `Could not open the MeMesh memories pane: ${error instanceof Error ? error.message : 'unknown reason'}` };
97 }
98 await search($, event.args.trim());
99 return {};
100 });
101 on('ui.render', { component: 'Pane' }, async ($, event, next) => {
102 if (event.requestId !== PANE) return next(event);
103 const { Box, Text, Button, Input } = $.ui.resolve(event);
104 const redraw = () => $.ui.invalidate('ui.render');
105 const typeName = memory => TYPE_NAMES[memory.type] || memory.type;
106 const day = memory => memory.created_at?.slice(0, 10) || 'date not recorded';
107 const oneLine = text => {
108 const flat = (text || '').replace(/\s+/g, ' ').trim();
109 return flat.length > 60 ? `${flat.slice(0, 59)}…` : flat;
110 };
111 const hint = text => Text({ children: [text], dimColor: true });
112 const where = folder && [
113 Text({ children: ['Project folder: ', Text({ children: [folder.split('/').filter(Boolean).at(-1) || folder], bold: true })] }),
114 // The full path only where there is room to spare: the list, not an open memory.
115 ...(selected ? [] : [hint(folder.replace(/^\/(Users|home)\/[^/]+/, '~'))]),
116 ];
117 const source = selected?.metadata?.provenance?.source_host;
118 const updateTitle = value => { if (!busy && draft) draft.title = value; };
119 const detail = selected && [
120 Text({ children: [selected.title || typeName(selected)], bold: true }),
121 Text({ children: [`${typeName(selected)} · created ${day(selected)} · `,
122 source ? Text({ children: [`written by ${HOST_NAMES[source] || source}`] }) : Text({ children: ['source not recorded'], color: 'warning' })], dimColor: true }),
123 // Actions sit above the text: a pane is only so tall, and a long memory would push them out of view.
124 Box({ flexDirection: 'row', gap: 2, marginTop: 1, children: draft ? [
125 Button({ key: 'memesh-save', label: 'Save changes', hotkey: 's', variant: 'primary', onPress: () => { void saveInPane($); } }),
126 Button({ key: 'memesh-cancel', label: 'Cancel', hotkey: 'c', onPress: () => {
127 if (!busy) { draft = undefined; notice = { text: 'Cancelled. Your edits were not saved.' }; redraw(); }
128 } }),
129 ] : [
130 // The form waits on a person; a press hook has 10 seconds, so the edit runs on and redraws when it settles.
131 Button({ key: 'memesh-edit', label: 'Edit this memory', hotkey: 'e', variant: 'primary', onPress: () => {
132 if (event.surface === 'desktop') void editInPane($);
133 else void edit($);
134 } }),
135 Button({ key: 'memesh-back', label: 'Back to list', hotkey: 'b', onPress: () => { if (!busy) { selected = undefined; notice = { text: '' }; redraw(); } } }),
136 ] }),
137 hint(draft ? 'Fill only what you want to change; blank keeps the original. Save changes or Cancel above.'
138 : event.surface === 'desktop' ? 'e edit in this pane · b back to list · Esc close'
139 : 'e edit · b back to list · Esc close'),
140 ...(!draft && event.surface !== 'desktop' ? [hint('Edit opens a form: fill in only what you want to change, blank keeps the original; Accept saves, Esc cancels.')] : []),
141 ...(draft ? [
142 Input({ key: 'memesh-title', label: 'New title (blank keeps original)', value: draft.title, autoFocus: true, submitLabel: 'keep editing',
143 onInput: updateTitle, onSubmit: updateTitle }),
144 ...draft.observations.map((text, index) => {
145 const update = value => { if (!busy && draft) draft.observations[index] = value; };
146 return Input({ key: `memesh-paragraph-${index}`, label: `Paragraph ${index + 1} (blank keeps original)`, value: text, submitLabel: 'keep editing', onInput: update, onSubmit: update });
147 }),
148 ] : []),
149 Box({ borderStyle: 'round', borderDimColor: true, paddingX: 1, children: [Text({ children: [selected.observations.join('\n\n')] })] }),
150 ...(selected.truncated ? [hint('Only part is shown here; Edit loads the full text.')] : []),
151 ];
152 const results = memories.map((memory, index) => Box({ flexDirection: 'column', children: [
153 Button({ key: `memesh-memory-${index}`, plain: true, hotkey: String(index + 1), label: memory.title || typeName(memory),
154 onPress: () => { selected = memory; notice = { text: '' }; redraw(); } }),
155 Text({ children: [` ${typeName(memory)} · ${day(memory)} — ${oneLine(memory.observations?.[0])}`], dimColor: true, wrap: 'truncate-end' }),
156 ] }));
157 const list = memories.length
158 ? [Text({ children: [`Found ${memories.length}. Press a number to open:`] }), ...results, hint('Esc to close')]
159 : [];
160 return Box({ flexDirection: 'column', children: [
161 ...(where || []),
162 ...(!draft ? [Box({ marginTop: 1, children: [Input({ key: 'memesh-search', label: 'Search', placeholder: 'type words and press Enter; leave empty for recent memories', value: query, submitLabel: 'search',
163 onInput: value => { query = value; }, onSubmit: value => search($, value) })] })] : []),
164 // Status sits above the results for the same reason as the actions.
165 ...(busy || notice.text ? [Text({ children: [busy ? 'Working…' : notice.text], ...(notice.tone && !busy ? { color: notice.tone } : { dimColor: true }) })] : []),
166 ...(!busy && notice.detail ? [hint(notice.detail)] : []),
167 Box({ flexDirection: 'column', marginTop: 1, children: detail || list }),
168 ] });
169 });
170}
171