🇧🇷 Leia em Português.
Persistent, shared memory for AI coding agents (Claude Code, and any MCP/REST-capable tool), backed by your own Supabase. Every session is auto-saved to a Postgres table and recalled at the start of the next one, across sessions, tool instances, and machines.
One stance sets it apart: memory you can trust, explain, and own. Not a black box.
- Explainable. Recall shows each item's provenance (a fact's confidence and age, a session's
session_id), and a fact's confidence decays with age, so stale memory fades on its own. - Human-gated. The cross-project developer profile and its rules are proposed, never applied on their own. You approve or reject; nothing silently rewrites how your agent works.
- Yours. Plain Postgres on your own Supabase.
pg_dumpanytime. No SaaS in the middle, no lock-in.
Claude Code starts every session from zero. Tools like claude-mem or mem0 solve this, but either
store locally (no cross-machine) or run through a hosted service, and most auto-apply whatever they
extract. agent-memory-hub is the self-owned, auditable take: a Supabase table you own, optional
layers you switch on as you need them (semantic search, facts, a cross-project developer profile,
backups), and a human in the loop wherever memory changes how the agent behaves.
- Cross-session / instance / machine: any setup pointing at the same Supabase shares everything.
- Distilled, not dumped: recall injects compact, ranked, explainable context, not raw transcripts.
- Self-improving, on your terms: it learns how you work across projects and proposes rules; you decide.
They don't compete: CLAUDE.md (and Cursor's AGENTS.md) are static instructions you
write by hand; agent-memory-hub is dynamic memory captured automatically. One answers
"how I want you to work" (conventions, canonical commands, rules); the other answers "what we
already did, decided, and broke before" — episodic context that doesn't fit a hand-kept file
because it changes every session.
CLAUDE.md / AGENTS.md |
agent-memory-hub |
|
|---|---|---|
| Nature | Static instructions, written by hand | Dynamic memory, captured automatically |
| Source | You type and maintain it | Generated from your real session history |
| Kind of knowledge | Rules ("always do it this way") | Episodic ("what happened / was decided / was fixed") |
| Cross-session | Reloads the same text every time | Recalls specific past sessions, on demand |
| Cross-machine | Only if you version the file | Automatic (shared Supabase) |
| Evolves itself? | No | Yes — captures, and proposes rules (you gate them) |
| Upkeep | You maintain it manually | Maintains itself |
Where each wins. CLAUDE.md is deterministic and versioned: a hand-written rule always
loads, identically, in every PR — right for commit conventions, build commands, module rules,
and anything that must be fixed. Memory is ranked and probabilistic, and shines where a static
file structurally can't: remembering automatically, across every session/machine/tool, auditably
and self-owned — so you stop re-explaining the project every session.
They connect. The two aren't rivals — the hub feeds CLAUDE.md. Its cross-project profile
watches your patterns, proposes rules, you approve, and they land in a profile-rules.md
your CLAUDE.md imports (@~/.claude/profile-rules.md). That's the loop that turns episodic
history into the static rules a CLAUDE.md holds — closing the gap between what happened and
how to always work. Use both: rules for what must be guaranteed, memory for everything you'd
otherwise forget.
- Auto-capture of every session, with a per-turn checkpoint that survives crashes.
- Secrets never persist: always-on redaction at capture masks private-key blocks, cloud/API
tokens and
NAME=valuecredentials before anything reaches the database, and anything you wrap in<private>...</private>in the conversation is never stored at all. - Recall at session start: a clean one-line summary per relevant session, plus the durable
facts for the current project. Each item carries its provenance (facts: confidence and
since-when; sessions: the
session_id) so recall is explainable. Fact confidence decays with age (per-kind half-life), so stale facts fade from recall without being deleted. - Recall on a token budget: the injected context has a hard cap (
RECALL_MAX_TOKENS, default 1500). Over budget it degrades gracefully — compact index first (full detail one MCPget_sessionaway), then lowest-priority items drop. Every injection is logged tohooks/recall.log(what went in, estimated cost, what was cut) — no blackbox memory, no burned token budgets. - Search your whole history: hybrid (keyword + semantic via
pgvector), with an optional LLM--reranksecond pass. - Facts layer (optional, bring-your-own-LLM): durable preferences / decisions / configs /
procedures (how-tos that worked) with temporal validity, deduped by meaning. A new fact
that contradicts an old near-match supersedes it at extraction time (
valid_until+superseded_by, non-destructive) instead of piling up conflicting memories. - Defrag job (
scripts/defrag_facts.py, optional): periodic sleep-time pass with your local LLM — supersedes duplicates, invalidates stale ephemeral facts. Non-destructive, conservative by default ("keep" when unsure). - Developer profile (optional): distills how you work across all your projects into a profile, and turns the patterns you approve into rules your agent follows. A self-improving loop, human-gated.
- Rules → enforcement (
scripts/enforce_rules.py, optional): approved rules that are mechanizable become a PreToolUse guard that blocks the violating shell command — a rule in markdown is a wish list; a hook is a contract. Human-gated: dry-run shows every regex, and it never edits yoursettings.jsonitself. - Cross-tool: Claude Code via hooks, Codex CLI and Cursor via adapters, any tool via the adapter template.
- MCP server (
scripts/mcp_server.py, pure stdlib): dedicated tools —recall_relevant,recent_sessions,get_facts,get_session— so any MCP agent (Claude Code, Cursor, Codex) queries the memory on-demand, with the task in hand, not just the passive recall at boot. - Memory console (
scripts/memory.py): browse, search and inspect from the terminal —stats,recent,search,facts,show,profile, plusstandup(what you touched today/this week),healthandlog(below). Installable as a globalmemcommand (pipx install -e .). - Markdown export (
mem export): a human-readable, git-versionable dump of facts, session summaries and approved rules — read, diff and commit what your agent knows. The database stays the source of truth; the export is the audit copy. - Procedures → skills (
mem skills): promotesprocedurefacts (how-tos that worked in a real session) to Claude CodeSKILL.mdfiles, closing the loop session → procedural fact → reusable skill. Dry-run by default, and it never overwrites an existing skill — once created, the file is yours to edit. - Health & observability (
memory.py health/log): reconciles local transcripts against Supabase and watches the capture error rate, so a silent capture failure surfaces instead of going unnoticed — memory you can verify, not just trust. - Weekly digest (
scripts/weekly_digest.py): a 7-day summary across all projects (LLM-free), with a hook into your content workflow. - Tested & measured: an offline pytest suite + CI (on every push/PR) pin the capture pipeline
so the silent-capture bugs can't return, and a recall eval harness (
scripts/eval_recall.py, hit@k / MRR) means recall quality is measured, not assumed — that's how we confirmed a recency bias would hurt and kept the 1:1 hybrid fusion as the calibrated default. - Your own backups: daily
pg_dumpto a portable.sql. No lock-in. - No LLM in the core (the "semantic" part uses an embedded model, not a chat LLM); every LLM-powered piece is optional and has a free option.
flowchart TD
subgraph S["AI coding session (Claude Code)"]
direction TB
SS["SessionStart"] --> RECALL["recall_session.py<br/>injects a digest of relevant past sessions"]
STOP["Stop (each turn)"] --> CAP["capture_session.py<br/>continuous checkpoint, background, upsert"]
SE["SessionEnd"] --> SAVE["capture_session.py<br/>final save"]
end
RECALL -.->|reads| DB[("Supabase, your project<br/>table public.sessions")]
CAP -->|writes| DB
SAVE -->|writes| DB
DB -->|optional| DUMP["pg_dump<br/>cron on an always-on host"]
DUMP --> GZ([".sql.gz"])
GZ --> PULL["pull to your machine"]
- Capture is idempotent (upsert by
session_id). TheStopcheckpoint means even an abrupt kill keeps the session up to its last turn. - Recall injects only a compact digest (one extractive summary line per session, not raw transcript). Full transcripts stay queryable on demand via the Supabase MCP or REST.
- The summary is deterministic and LLM-free: the capture hook keeps the first substantive
user ask, the last one, and turn counts. Run
sql/04-summary.sqlto add the column.
The design rule: collecting and reading are automatic; anything that changes how your agent
behaves goes through you. The split is deliberate — it's this project's defense against
memory poisoning (see the non-goals in ROADMAP.md). mem help prints the
whole map in the terminal.
Automatic — hooks; you never run anything:
| What | When it runs |
|---|---|
| Session capture (per-turn checkpoint + final save) | every Stop / SessionEnd |
Secrets redaction + <private> stripping |
inside capture, always |
| Recall injection (facts + relevant sessions) | every SessionStart |
Token budget + injection log (hooks/recall.log) |
inside recall, always |
| Temporal fact supersession | inside facts extraction, whenever it runs |
Semi-automatic — built for cron; run or schedule them yourself:
| What | Command | Note |
|---|---|---|
| Facts extraction | python3 scripts/extract_facts.py |
needs your LLM reachable (e.g. local Ollama) |
| Re-extract old sessions | mem reprocess [how-to|all] |
one command: resets + loops until drained (dedup keeps it safe). Add --embed to also fill session embeddings |
| Defrag / reflection | python3 scripts/defrag_facts.py |
same; non-destructive |
| Pending embeddings | python3 scripts/embed_pending.py |
no LLM — just calls the Supabase embed function; ideal for an EC2 cron (only needs EMBED_KEY) |
Manual on purpose — human-gated (each one is dry-run by default):
| What | Command | Why a human gate |
|---|---|---|
Patterns → rules in CLAUDE.md |
mem profile (approve) then apply_profile_rules.py --write |
a rule changes how the agent acts everywhere |
| Rules → blocking hook | enforce_rules.py --write |
one wrong regex blocks a legitimate command |
| Procedures → skills | mem skills --write |
a skill enters your agent's context |
| Markdown export | mem export |
on-demand snapshot |
The memory itself is just Postgres, so what's tool-specific is only the automatic capture/recall, which ships as Claude Code hooks.
- Auto-capture + recall (the hooks): Claude Code. The
hooks use its
SessionStart,Stop, andSessionEndevents. - Read and query the shared memory: any MCP or REST capable AI tool, for example Cursor, Codex CLI, Gemini CLI, or ChatGPT, through the Supabase MCP or the REST API.
- Capture from another tool: an adapter scans that tool's local transcripts and uploads
them. Codex CLI and Cursor ship as adapters (
scripts/adapters/); see Capture from other tools to add more.
Also needed:
- A free Supabase project.
python3(hooks and backup are pure stdlib, no pip needed).
Shortcut: after cloning and filling
.env, run./scripts/setup.sh. It applies the SQL migrations and installs the Claude Code hooks in one idempotent step (it does not enable the optional LLM facts layer). The manual steps below explain what it does.
git clone https://github.com/carloshpdoc/agent-memory-hub.git
cd agent-memory-hubAt supabase.com: new project. Enable Data API and RLS. Grab from Settings > API: Project URL, publishable key, secret key.
Open SQL Editor in Supabase and run sql/01-schema.sql.
It creates the sessions table, the full-text index, and RLS.
cp .env.example .env
# edit .env with your SUPABASE_URL and SUPABASE_SECRET_KEY (and backup vars if used).env is gitignored. The hooks read it directly.
Add to your Claude Code settings.json (~/.claude/settings.json for user scope), using the
absolute path to your clone:
{
"hooks": {
"SessionStart": [
{ "matcher": "", "hooks": [
{ "type": "command", "command": "python3 /ABS/PATH/agent-memory-hub/hooks/recall_session.py", "timeout": 15 }
]}
],
"Stop": [
{ "matcher": "", "hooks": [
{ "type": "command", "command": "payload=$(cat); printf '%s' \"$payload\" | python3 /ABS/PATH/agent-memory-hub/hooks/capture_session.py >/dev/null 2>&1 &" }
]}
],
"SessionEnd": [
{ "matcher": "", "hooks": [
{ "type": "command", "command": "python3 /ABS/PATH/agent-memory-hub/hooks/capture_session.py", "timeout": 20 }
]}
]
}
}If you already have hooks for these events, add these entries to the existing arrays.
Lets the agent query memories interactively:
claude mcp add --scope user --transport http supabase \
"https://mcp.supabase.com/mcp?project_ref=<YOUR_PROJECT_REF>"
# then authenticate: /mcp > supabaseOn an always-on host with pg_dump at or above your Postgres major version, and the repo cloned:
- Put your pooler creds in
~/.pgpass(chmod 600):HOST:5432:postgres:postgres.<PROJECT_REF>:PASSWORD - Fill the
PG_POOLER_*vars in.env. - Cron:
30 3 * * * /ABS/PATH/agent-memory-hub/scripts/backup.sh >> .../backup.log 2>&1 - Pull copies locally with
scripts/pull-backups.sh(setREMOTE_SSHandSSH_KEYin.env).
This is the whole point, and it's trivial:
- Clone the repo on the new machine.
- Copy the same
.env(same Supabase credentials). - Run
./scripts/setup.sh.
Done. That machine now writes to and reads from the same shared memory. The migrations are idempotent (and skipped if it has no DB creds, since the schema is shared). The optional facts layer stays off, so a weaker machine just captures and reads, while heavy fact extraction runs only where you enable it.
To also upload that machine's prior Claude Code history (sessions from before the hooks),
run python3 scripts/backfill_sessions.py --dry-run to preview, then without the flag to upload.
It is idempotent (skips sessions already in Supabase).
The Claude Code hooks are one capture path. Tools without lifecycle hooks are handled by an
adapter that scans their local transcripts and uploads new ones (idempotent), the same way
backfill_sessions.py works for Claude Code. Adapters run on a cron and write with tool=<name>,
so recall, search and facts treat all tools uniformly.
- Codex CLI (
scripts/adapters/codex.py) reads~/.codex/sessions/**/rollout-*.jsonl. Run with--dry-runto preview, then put it on a cron. - Cursor (
scripts/adapters/cursor.py) reads Cursor's chat from its SQLite store (.../Cursor/User/globalStorage/state.vscdb), reconstructing each conversation from its message bubbles. A settle guard skips chats still in flight.--dry-runto preview; override the DB path withCURSOR_DB=...on another OS. Then put it on a cron. - Adding a tool: write a small adapter that maps the tool's transcripts to
(session_id, cwd, user/assistant turns)and upserts withtool=<name>. Usecodex.py(JSONL) orcursor.py(SQLite) as the template. Gemini CLI is a good first contribution.
| Var | Used by | Meaning |
|---|---|---|
SUPABASE_URL |
hooks, backup.py | https://<ref>.supabase.co |
SUPABASE_SECRET_KEY |
hooks | service_role key (writes, bypasses RLS) |
PG_POOLER_HOST, PG_POOLER_USER |
backup.sh | Session Pooler host, postgres.<ref> |
BACKUP_DIR, KEEP |
backup.sh, backup.py | output dir, how many to keep |
REMOTE_SSH, SSH_KEY |
pull-backups.sh | always-on host, SSH key |
EMBED_KEY |
embed_pending.py, search.py | guard for the embedding function (Phase 2) |
- Console:
python3 scripts/memory.pyopens an interactive prompt, or use it as subcommands:stats,recent [N],search [--project P] "<q>",facts [project],show <id-prefix>,standup [today|yesterday|week],health,log [N]. Pure stdlib, no server, no key in a browser. Either alias it (alias mem='python3 /ABS/PATH/agent-memory-hub/scripts/memory.py') or install a realmemcommand from your clone:pipx install -e .(orpip install -e .). Editable keeps the files in the clone, so config andmem healthresolve as usual; config is read from$AGENT_MEMORY_HUB_ENV, then~/.config/agent-memory-hub/.env, then the repo.env. - MCP server (dedicated):
scripts/mcp_server.pyexposesrecall_relevant,recent_sessions,get_facts,get_sessionover stdio/JSON-RPC (pure stdlib, no deps), so any agent queries the memory on-demand with the task in context — not only the passive recall at session start:claude mcp add --scope user agent-memory-hub -- python3 /ABS/PATH/agent-memory-hub/scripts/mcp_server.py
- Supabase MCP: ask the agent. It runs SQL via the Supabase MCP.
- REST full-text:
GET /rest/v1/sessions?content_tsv=fts(simple).<term>with the secret key. - Filters: by
project,machine,started_at,session_id.
Optional. Adds meaning-based recall on top of full-text search, using pgvector and the
gte-small model running inside a Supabase Edge Function (free, no external API).
- Run
sql/02-phase2-pgvector.sql. It adds theembeddingcolumn, the HNSW index, and thematch_sessionsRPC. Also runsql/03-hybrid-search.sqlfor thehybrid_searchRPC. - Set a guard secret and deploy the function:
Put the same
supabase secrets set EMBED_KEY=$(openssl rand -hex 24) supabase functions deploy embed --no-verify-jwt # new API keys are not JWTs
EMBED_KEYin your.env. - Embed existing rows:
python3 scripts/embed_pending.py. Run it on a cron to keep new sessions embedded (for example*/15 * * * *on your always-on host). - Search:
python3 scripts/search.py "how did we set up backups". It runs hybrid search (keyword + semantic, fused with Reciprocal Rank Fusion), so exact terms that pure vector search would miss still surface, and vice versa. Add--rerankfor an optional LLM second pass that reorders the top candidates by relevance (needsFACTS_LLMset).
The Edge Function returns only vectors and counts, never session content.
The same stance the tool takes on capture, applied to recall itself: measure whether it
surfaces the right past context, don't assume it. scripts/eval_recall.py
runs the real recall path and scores it with hit@k and MRR.
python3 scripts/eval_recall.py --auto 30 # retrieval regression check
python3 scripts/eval_recall.py --auto 60 --spread # representative, corpus-spread sample
python3 scripts/eval_recall.py --gold tests/eval/recall_gold.example.json--auto Nsamples N recent sessions, turns each session's own summary into a query, and checks whether that session comes back near the top. It won't prove recall is smart, but it screams when recall is broken (embeddings down, FTS misconfigured) — the silent failure this project exists to catch. Add--spreadto sample across the whole corpus instead of the most recent (deterministic, reproducible) — that's the representative measurement mode.--gold FILEscores curated{query, expect:{project?, contains?}}cases (gold cases are your own; the shipped file is a format example).
Measured on the author's real corpus (267 sessions across 47 projects, hybrid recall,
--auto 60 --spread, Jul 2026):
| metric | score |
|---|---|
| hit@1 | 46.7% |
| hit@3 | 58.3% |
| hit@5 | 68.3% |
| MRR | 0.540 |
i.e. given only a one-line summary of a past session as the query, the exact session comes
back in the top 5 two times out of three, against 266 distractors. Your numbers will vary
with your corpus — the point is that you can measure yours with one command. (The harness
also killed a plausible "improvement": recency weighting, measured at −27 to −45 pp hit@1,
rejected. See docs/00-design-decisions.md.)
Everything above needs no LLM (the "semantic" part uses the embedded gte-small, not a
chat model). This optional layer is the only part that uses an LLM, and it is
bring-your-own-LLM with free options, so it never forces a cost or a specific provider.
When enabled, an optional cron distills each session into durable atomic facts (preferences, decisions, configs) with temporal validity, deduped by meaning. Recall then injects the relevant facts (current project + global) at the top of the digest.
- Run
sql/05-facts.sql(facts table, validity model,match_factsRPC). - Pick a provider in
.envviaFACTS_LLM:ollama: local, free, private (needs Ollama running).gemini: Google AI Studio free tier (GEMINI_API_KEY).openai: OpenAI or any OpenAI-compatible endpoint (Groq, OpenRouter, local).off(default): disabled; the rest of the tool is unaffected.
- Run
python3 scripts/extract_facts.py(put it on a cron to process new sessions).
Deduping facts (optional, manual). Over time, facts can overlap. sql/06-find-fact-dupes.sql
plus scripts/consolidate_facts.py let an LLM judge near-duplicate pairs and supersede the
redundant one (non-destructively, via valid_until). Run it with --dry-run and review:
this is deliberately not automated, because a weak judge can over-merge useful specifics, so a
human should approve. Some redundancy is fine; only prune what you've checked.
Most memory tools store facts per project. Because this hub holds every session across all your projects, it can do something they can't: distill a profile of how you work and turn the high-confidence patterns into rules your agent then follows. The loop is: the more you use it, the better-tuned the agent's own instructions get, with you as the gate.
Illustrative run with generic data. The loop: synthesize a profile, review each pattern, approve, and the approved rules land in a file your
CLAUDE.mdimports.
- Run
sql/07-profile.sql(theprofile_patternstable). - With
FACTS_LLMset (same providers as above), runpython3 scripts/synthesize_profile.py. It reads your durable facts, keeps only patterns with evidence in 2+ different projects, and writes a human-readablePROFILE.md. High-confidence patterns get a proposed rule. - Review with
python3 scripts/memory.py profileand approve or reject each (profile approve <id>/profile reject <id>). Curation is deliberately human. - Run
python3 scripts/apply_profile_rules.py(dry-run) then--write. It writes the approved rules to a separate file (~/.claude/profile-rules.md), never touching your hand-writtenCLAUDE.md. Load them once by adding@~/.claude/profile-rules.mdtoCLAUDE.md.- Add
--per-projectto instead write one file per project (~/.claude/profile-rules/<project>.md), each with the rules whose evidence includes that project. Import the relevant one from that repo'sCLAUDE.md, so project-specific rules don't load in unrelated sessions.
- Add
Recall also proactively surfaces any detected-but-unreviewed patterns at the start of a session (with their confidence and project count), so the loop closes itself instead of waiting for you to remember to run the review.
Like the facts layer, this is optional, bring-your-own-LLM, and gated by human review: a weak judge over-merges, and "newer" is not always "better", so nothing is applied without your sign-off.
- Secrets live only in
.envand~/.pgpass(gitignored, chmod 600). Never commit them. - RLS is on. The public (anon) key can't read without a policy. Hooks use the secret key.
- The secret key is powerful. Treat it like a password.
scripts/setup.sh one-shot setup/update: migrations + hooks (idempotent)
scripts/migrate.py apply sql/*.sql to Supabase
scripts/install_hooks.py add the hooks to settings.json
scripts/backfill_sessions.py upload this machine's prior Claude Code history (one-time)
scripts/adapters/codex.py capture adapter for Codex CLI (JSONL template)
scripts/adapters/cursor.py capture adapter for Cursor (SQLite template)
scripts/memory.py memory console: browse/search/inspect/standup/health (Phase 8)
scripts/memory_client.py shared Supabase access + read queries (used by console + MCP server)
scripts/mcp_server.py MCP server (stdio/JSON-RPC, pure stdlib): recall_relevant, get_facts, ...
scripts/weekly_digest.py 7-day cross-project digest, LLM-free (Phase 10)
hooks/capture_session.py capture (Stop + SessionEnd)
hooks/recall_session.py recall (SessionStart)
sql/01-schema.sql table + full-text + RLS
sql/02-phase2-pgvector.sql pgvector + match_sessions RPC (Phase 2)
sql/03-hybrid-search.sql hybrid_search RPC: keyword + semantic via RRF (Phase 3)
sql/04-summary.sql summary column (extractive, LLM-free)
sql/05-facts.sql facts/preferences layer + match_facts RPC (Phase 4, optional)
supabase/functions/embed/ gte-small embedding Edge Function (Phase 2)
scripts/backfill_summaries.py fill summary for existing rows (one-time)
scripts/extract_facts.py distill sessions into facts via your LLM (Phase 4, optional)
scripts/consolidate_facts.py review/merge duplicate facts via LLM (Phase 5, manual)
sql/06-find-fact-dupes.sql find near-duplicate fact pairs (Phase 5)
sql/07-profile.sql developer-profile patterns table (Phase 9, optional)
scripts/synthesize_profile.py distill a cross-project dev profile via LLM (Phase 9, optional)
scripts/apply_profile_rules.py write approved profile rules to ~/.claude/profile-rules.md (Phase 9)
scripts/backup.sh pg_dump backup (cron on an always-on host)
scripts/pull-backups.sh rsync backups to this machine
scripts/backup.py portable logical backup (REST/NDJSON, no pg client)
scripts/embed_pending.py embed sessions missing a vector (Phase 2)
scripts/search.py semantic search CLI (Phase 2)
scripts/eval_recall.py recall eval harness: hit@k / MRR (auto + gold modes)
scripts/mem_cli.py console entry point for the installed `mem` command
tests/ offline pytest suite (capture, cursor adapter, eval) + CI
docs/ architecture and design decisions
If this saved you from re-explaining your project to your agent for the tenth time today, give the repo a star. It genuinely helps other people find it.
Ideas, rough edges, or a capture adapter for your tool (Gemini CLI, Windsurf, Zed...)? Open an issue or a pull request — see CONTRIBUTING.md for the dev setup and the adapter guide, and ROADMAP.md for what's planned (including some good first issues). If you build something on top of agent-memory-hub, I would love to see it.
Made by buildcomcarlos.com: articles and open tools on AI agents, iOS, and shipping software solo. If this project was useful, the site is where the deep dives and the next experiments live. Come say hi.

