SLOPSHOPPER

project-memory

A repo's memory/ folder, shared through git: a Haiku agent reads it and returns only what the task needs, another one compacts it

newpromptagents
★ 3v0.2.0MITupdated 2026-10-09noash-xrc/claude-tools/project-memory
A shopper browsing a rack in a slop shop
README

Project memory

A Claude Code mod for a repo that keeps shared memory in memory/, committed with the code. A Haiku agent reads the memory and hands the main session only what bears on the task, so a 26KB memory file costs the main model a few lines instead of its whole length.

The mod switches on in a repo that has memory/MEMORY.md and stays out of the way everywhere else.

What's in it

PartWhat it does
memory-recall agent (Haiku, read-only)Given a task, reads the index and the matching files. Returns rejected approaches word for word, the relevant facts with their source files, and which files to read in full
memory-keeper agent (Haiku)Compacts the index, merges duplicates, fixes index lines that point nowhere, and files a memory whose text the main session wrote
project-memory skillThe file format and the rules for saving. Saving stays with the main session because it needs the conversation
HookFinds the nearest memory/MEMORY.md at or above the working directory. If the index is under 4KB it goes straight into the system prompt and memory-recall is hidden. A bigger one gets a short section telling Claude to ask memory-recall before a non-trivial task. Lines marked REJECTED or "don't re-propose" in any memory file go into the system prompt either way, and an index over 16KB gets a nudge to compact. Without a memory folder it hides both agents

A recall on a 136-memory folder took about 40 seconds and $0.02 of Haiku, and returned about 2k tokens.

Layout

memory/
  MEMORY.md          index, one line per memory: - [Title](slug.md) — hook
  some-decision.md   one memory, with name / description / metadata.type frontmatter

The skill has the full format. A repo with no memory/ folder needs one with a MEMORY.md in it before the mod switches on.

Codex

Codex can't load the mod, but the files are plain Markdown, so it reads and writes the same folder. Put the reading and saving rules in the repo's AGENTS.md: read memory/MEMORY.md before a non-trivial task, open the matching files, and save in the format above. Claude Code doesn't read AGENTS.md, so the two sets of instructions don't collide. The keeper's compacting step catches a memory Codex wrote without an index line.

Moving from a per-repo setup

If the repo's CLAUDE.md tells Claude to read memory/MEMORY.md, or the repo has its own project-memory skill, remove them. The mod's prompt section replaces the first, and the skills would otherwise both show up.

Install

/plugin install project-memory --marketplace noash-xrc/claude-tools
Source 1 files
hooks/register.ts 59 lines
1import type { Register } from 'claude-code'
2
3// Under this, the index goes straight into the prompt: cheaper than a recall's answer, and no 40-second wait.
4const INLINE_MAX = 4 * 1024
5// Over this, the index needs compacting. The skill and memory-keeper use the same number.
6const COMPACT_AT = 16 * 1024
7const REJECTED = /REJECTED|don't re-propose/i
8
9const bytes = (s: string) => new TextEncoder().encode(s).length
10
11type Memory = { dir: string; index: string; rejected: string[] }
12
13// Read once per session start: a repo gains a memory folder rarely enough that a reload catches it.
14let memory: Memory | undefined
15
16const isSmall = (m: Memory) => bytes(m.index) <= INLINE_MAX
17
18const sectionText = (m: Memory) => [
19  '# Project memory',
20  `This repo keeps shared memory in \`${m.dir}\`, one file per memory, indexed in \`MEMORY.md\` there.`,
21  isSmall(m)
22    ? `Before a non-trivial task, read the memory files whose index lines bear on it. The index:\n\n${m.index.trim()}`
23    : `Before a non-trivial task, ask the \`project-memory:memory-recall\` agent what the memory says about the task, and give it the folder's path. Don't read the folder yourself unless its answer points you to a file you need in full.`,
24  ...(m.rejected.length ? ['Rejected approaches. Never re-propose these:', ...m.rejected] : []),
25  ...(bytes(m.index) > COMPACT_AT ? [`The index is over 16KB. At a pause in the work, hand compacting to the \`project-memory:memory-keeper\` agent.`] : []),
26  'To save a memory, follow the `project-memory` skill. To compact the index or clean up the folder, hand it to the `project-memory:memory-keeper` agent.',
27].join('\n')
28
29export const register: Register = on => {
30  on('session.start', async ($, e, next) => {
31    memory = undefined
32    // The nearest memory/MEMORY.md at or above the working directory, so a session started in a subfolder finds it.
33    const found = (await $.fs.ancestors({ names: ['memory/MEMORY.md'] }).catch(() => [])).at(-1)
34    if (found) {
35      const dir = `${found.dir.replace(/[\\/]$/, '')}/memory`
36      const files = (await $.fs.list(dir).catch(() => []))
37        .filter(f => f.kind === 'file' && f.name.endsWith('.md') && f.name !== 'MEMORY.md')
38      const rejected = (await Promise.all(files.map(async f => {
39        const text = await $.fs.read(`${dir}/${f.name}`).catch(() => '')
40        return text.split('\n').filter(l => REJECTED.test(l)).map(l => `- ${l.trim()} (memory/${f.name})`)
41      }))).flat()
42      memory = { dir, index: found.parts[0]?.content ?? found.content, rejected }
43    }
44    return next(e)
45  })
46
47  on('prompt.compose', async ($, e, next) => {
48    const r = await next(e)
49    return memory ? { sections: [...r.sections, { id: 'project-memory:recall', scope: 'session', text: sectionText(memory) }] } : r
50  })
51
52  // The agents only make sense where there is a memory folder, and recall only where the index is too big to inline.
53  on('agent.offer', async ($, e, next) => {
54    if (!e.agent.startsWith('project-memory:')) return next(e)
55    if (!memory || (e.agent === 'project-memory:memory-recall' && isSmall(memory))) return { isOffered: false }
56    return next(e)
57  }).catch(($, e, next) => next(e))
58}
59