Persistent memory for AI agents. No vector DB. No daemon. Just git.
Why mneo exists and what it refuses to be: MANIFESTO.md.
Your coding agent forgot what it figured out in feat/auth yesterday. The internet's answer is a vector database, an embedding pipeline, a daemon, and a $20/mo SaaS.
git already does this.
$ git show refs/agent-memory/feat-auth/oauth-flow:note.md
Rejected JWT — session cookies for SSR. Don't re-propose.Branches are scopes. Refs are storage. push and fetch are sync. Five verbs, zero infrastructure.
record({ body }) // save what worked
list({}) // headlines on this branch
read({ slug }) // expand a headline
copy({ from, to, slug }) // promote past merge
forget({ slug }) // outdatedShips as an MCP server for Claude Code, or a TypeScript SDK for any agent. ~1500 lines of TypeScript. Zero runtime deps in the SDK. The notes survive uninstall.
refs/agent-memory/feat-auth/oauth-flow
refs/agent-memory/main/architecture
refs/agent-memory/main/db-schema
One git ref per note. Slashes in branch names become dashes (feat/auth → feat-auth). The note's first line becomes the commit subject — that's what list returns as headlines.
Inspect like code:
git log refs/agent-memory/feat-auth/oauth-flow
git show refs/agent-memory/feat-auth/oauth-flow:note.mdSync like code:
git push origin 'refs/agent-memory/*:refs/agent-memory/*'
git fetch origin '+refs/agent-memory/*:refs/agent-memory/*'npm install -g mneo mneo-mcp
# or zero-install:
npx -y mneo <subcommand>Requires Node >=18 and git >= 2.31.
One command:
cd your-project
npx -y mneo installThree pieces land in .claude/:
- A session-start hook that auto-injects recent headlines on
startup | resume | clear | compact. Failures exit 0 — broken memory never blocks the agent. - An MCP server exposing
record,list,read,forget,copy,push,fetch. - A skill file at
.claude/skills/mneo/SKILL.mdtelling the model when to call which verb.
Atomic and idempotent. Re-running mneo install reports each piece as unchanged; unrelated settings.json keys are preserved.
Manual install (no orchestrator)
npx -y mneo init-hook # 1. SessionStart hook only# 3. Skill file
mkdir -p .claude/skills/mneo
ln -s "$(pwd)/SKILL.md" .claude/skills/mneo/SKILL.mdFor agents in bash, Python, Rust — anything that can spawn a process. --json for machines, human one-liners by default. Exit 1 on MneoError (code on stderr), 2 on usage.
echo "decision body" | mneo record --slug oauth/flow --json
mneo list --prefix oauth/ --limit 5 --json
mneo read --slug oauth/flow --json
mneo copy --from feat-x --to main --slug oauth/flow --json
mneo forget --slug oauth/flow --jsonrecord reads the body via --body or stdin. All verbs accept --scope to override the auto-detected branch. copy takes --from and --to, plus either --slug (single) or --prefix (bulk); omit both to copy the whole source scope.
Note bodies become trusted prompt context for every agent turn. Scope is the trust boundary. Anyone with write access to refs/agent-memory/<scope>/* can plant a note your agent will read on the next session and treat as instructions.
refs/agent-memory/* is not pushed by the default refspec — sync is opt-in. If you wire it, treat memory pushes like code pushes.
Defenses:
- The session-start hook wraps the bundle in
<mneo-memory>tags telling the model to treat the contents as data, not instructions. Always on. - Set
MNEO_REQUIRE_SIGNED=1to gatelistandreadongit verify-commit. Recommended whenever you fetchrefs/agent-memory/*from a remote you don't fully control.
Full threat model: SECURITY.md.
| Var | Default | Use |
|---|---|---|
MNEO_REPO |
walks up from cwd | repo path override |
MNEO_SCOPE |
current branch | CI / detached HEAD / branch-collision disambiguation |
MNEO_AUTHOR |
mneo <agent@mneo> |
stamp commits with the agent's identity |
MNEO_REQUIRE_SIGNED |
unset | 1 or true → reject unsigned notes (UntrustedError) |
Per-call author override: pass by to record.
- The LLM ranks better than any embedding model on prompt-context retrieval.
list → readis enough. - Vector DBs need a daemon, an index, and an embedding API. Git is already on every dev's machine.
- Branches give scoping for free. RAG re-implements this with metadata filters.
- Tags, importance, facets — encode them in the slug. Convention beats taxonomy.
- No TTL.
git gcreclaims unreachable objects aftergc.pruneExpire(default 14 days post-forget).
The API is shaped for an LLM caller: tool descriptions are model instructions, errors are recovery prompts, sentinels (*) live in a namespace disjoint from valid inputs. There is no UI to optimize for.
| body | 5000 chars |
| slug | ^[a-z0-9][a-z0-9-/]*$, ≤80 chars |
| scope | ^[a-z0-9][a-z0-9-]*$, ≤80 chars |
| headline | first 80 chars of body |
record is idempotent: same (scope, slug, body) returns { unchanged: true } and creates no new commit.
list({}) defaults to last 30 days, max 50 entries. The result carries hidden / untrusted / skewed / more counters so callers can detect when filters dropped data — bump maxAgeDays or limit to surface it.
forget({ scope: "*" }) removes the slug from every scope. To purge a leaked secret immediately:
git reflog expire --expire=now --all && git gc --prune=nowgit clone git@github.com:HugoLopes45/mneo.git
cd mneo
bun install
bun test
bun run lintBun >=1.3.0. bun.lock is canonical. Branches: feat/<short-name> from main. See CONTRIBUTING.md.
Inspired by Mourad Ghafiri's git-notes-memory.
MIT.